fix scrolling

fix scrolling

--- reflection.coffee
+++ reflection.coffee
@@ -5,101 +5,71 @@
 		@editor = CodeMirror(document.body,
 			lineNumbers: true
 			autofocus: true
-			value: state or "#Reflection ver 0.1"
+			value: state or "#Reflection: Coffeescript editor"
 			onCursorActivity: => @editor.matchHighlight "CodeMirror-matchhighlight"
 			onChange: @reflect
 		)

-		$("<canvas>").attr('id', 'scroll').appendTo(document.body)
-
-		$(document.body).append("MWM")
-
 		@width = 80
-		@height = $(window).height()
-		canvas = $("#scroll").attr("width", @width).attr("height", @height)
-		@context = canvas[0].getContext("2d")
+		@height = Math.max($(window).height(), @editor.lineCount())
+
+		@canvas = $("<canvas>")
+		.attr('id', 'scroll')
+		.attr("width", @width)
+		.attr("height", @height)
+		.appendTo(document.body)
+
+		@context = @canvas[0].getContext("2d")
 		@frame = {@data} = @context.createImageData(@width, @height)
-		@scrollTop = $(window).scrollTop()
-		@scrollBottom = @scrollTop + @height
+		scrollTop = $(window).scrollTop()
+		@top = @editor.coordsChar(x: null, y: scrollTop).line
+		@bottom = @editor.coordsChar(x: null, y: scrollTop + $(window).height()).line + 1
+
 		@reflect()
-
-		@editor.setCursor({line: @editor.lineCount(), ch: 0})

 		scrollKeys = {}
 		keyMap = CodeMirror.keyMap["default"]
-		keyMap["Ctrl-1"] = -> scrollKeys["1"] = $(window).scrollTop()
-		keyMap["1"] = -> $(window).scrollTop(scrollKeys["1"])
-		keyMap["Ctrl-2"] = -> scrollKeys["2"] = $(window).scrollTop()
-		keyMap["2"] = -> $(window).scrollTop(scrollKeys["2"])
-		keyMap["Ctrl-3"] = -> scrollKeys["3"] = $(window).scrollTop()
-		keyMap["3"] = -> $(window).scrollTop(scrollKeys["3"])
-		keyMap["Ctrl-4"] = -> scrollKeys["4"] = $(window).scrollTop()
-		keyMap["4"] = -> $(window).scrollTop(scrollKeys["4"])
+		keyMap["Shift-Ctrl-1"] = -> scrollKeys["1"] = $(window).scrollTop()
+		keyMap["Ctrl-1"] = -> $(window).scrollTop(scrollKeys["1"])

 		CodeMirror.commands.save = => @downloadAs("name.coffee")
-
-		dragging = null
+		CodeMirror.commands.open = => @downloadAs("name.coffee")
+
+		@dragging = null

 		$(window)
 		.scroll(@scroll)
-		.mousedown((event) -> dragging = true)
-		.mouseup((event) -> dragging = false)
-		.mouseleave((event) -> dragging = false)
-
-		$("#scroll")
-		.click((event) =>
-			y = event.clientY
-			top = @editor.charCoords(
-				line: Math.max(0, y - 20)
-				ch: 0
-			).y
-			$(window).scrollTop top
-		)
-		.mousemove (event) =>
-			if dragging
-				y = event.clientY
-				top = @editor.charCoords(
-					line: Math.max(0, y - 20)
-					ch: 0
-				).y
-				$(window).scrollTop top
-
-	setPixel: (x, y, r, g, b, a) ->
-		index = (x + y * @width) * 4
-		@data[index + 0] = r
-		@data[index + 1] = g
-		@data[index + 2] = b
-		@data[index + 3] = a
-
-	charAlpha: (ch) ->
-		if /[._'"\-]/.test(ch)
-			150
-		else if /[flyg]/.test(ch)
-			225
-		else if /[a-z]/.test(ch)
-			200
-		else
-			255
+		.resize(@resize)
+		.mousedown((event) => @dragging = true)
+		.mouseup((event) => @dragging = false)
+		.mouseleave((event) => @dragging = false)
+
+		@canvas
+		.mouseup(@scrollClick)
+		.mousemove(@scrollSlide)
+
+		$(document.body).append("MWM")

 	reflect: (editor, change) =>
 		t0 = (+new Date)
-		x = col = 0
-		y = 0
-		end = @height
-		first = @editor.coordsChar(x: null, y: @scrollTop).line
-		last = @editor.coordsChar(x: null, y: @scrollBottom).line + 1
-		if change
+
+		@lines = @editor.lineCount()
+		if not change
+			x = col = 0
+			y = 0
+			end = @lines
+		else
 			x = col = change.from.ch
 			y = change.from.line
 			linesChanged = change.to.line + 1 - y
-			end = Math.max(end, y + linesChanged + 1)
-			end = y + linesChanged if linesChanged is change.text.length
+			end = Math.max(@lines, y + linesChanged + 1)
+			end = y + linesChanged + 1 if linesChanged is change.text.length
 		while y < end
 			while col < @width
 				line = @editor.getLine(y)
 				ch = line?.charAt(col)
 				if ch and /\t/.test(ch)
-					if y > first and y < last
+					if @top < y < @bottom
 						@setPixel x, y, 200, 225, 255, 125
 						@setPixel x + 1, y, 200, 225, 255, 125
 					else
@@ -125,7 +95,7 @@
 					else
 						@setPixel x, y, 0, 0, 0, 255
 				else
-					if y > first and y < last
+					if @top < y < @bottom
 						@setPixel x, y, 200, 225, 255, 125
 					else
 						@setPixel x, y, 0, 0, 0, 0
@@ -135,22 +105,26 @@
 			++y
 		@context.putImageData @frame, 0, 0
 		localStorage.setItem("state", @editor.getValue())
-		console.log (+new Date) - t0 + "ms"
+
+		console.log (+new Date) - t0 + "ms", 'reflect'

 	scroll: =>
-		@scrollTop = $(window).scrollTop()
-		@scrollBottom = @scrollTop + @height
-		lines = @editor.lineCount()
-		end = Math.min(@height, lines)
-		first = @editor.coordsChar(x: null, y: @scrollTop).line
-		last = @editor.coordsChar(x: null, y: @scrollBottom).line + 1
-		y = 0
+		t0 = (+new Date)
+
+		top = @top
+		bottom = @bottom
+		height = $(window).height()
+		scrollTop = $(window).scrollTop()
+		@top = @editor.coordsChar(x: null, y: scrollTop).line
+		@bottom = @editor.coordsChar(x: null, y: scrollTop + height).line + 1
+		y = Math.min(top, @top)
+		end = Math.max(bottom, @bottom)
 		while y < end
 			x = col = 0
 			while x < @width
-				ch = @editor.getLine(y).charAt(col)
+				ch = @editor.getLine(y)?.charAt(col)
 				if ch and /\t/.test(ch)
-					if y > first and y < last
+					if @top < y < @bottom
 						@setPixel x, y, 200, 225, 255, 125
 						@setPixel x + 1, y, 200, 225, 255, 125
 					else
@@ -158,7 +132,7 @@
 						@setPixel x + 1, y, 0, 0, 0, 0
 					++x
 				else if not ch or /\s/.test(ch)
-					if y > first and y < last
+					if @top < y < @bottom
 						@setPixel x, y, 200, 225, 255, 125
 					else
 						@setPixel x, y, 255, 255, 255, 125
@@ -166,9 +140,43 @@
 				++col
 			++y
 		@context.putImageData @frame, 0, 0
-		#if lines > @height
-			#offset = -(first / lines * (lines - @height)) | 0
-			#$("#scroll").css('top', offset + 'px')
+		if @lines > height
+			@offset = -(@top / @lines * (@lines - height)) | 0
+			$("#scroll").css('top', @offset + 'px')
+
+		console.log (+new Date) - t0 + "ms", 'scroll'
+
+	scrollClick: (event) =>
+		return if @dragging
+		y = event.clientY - 20
+		if @lines > @height
+			top = y / @height * $(".CodeMirror").height() | 0
+		else
+			top = @editor.charCoords(
+				line: Math.max(0, y)
+				ch: 0
+			).y
+		$(window).scrollTop top
+
+	scrollSlide: (event) =>
+		return if not @dragging
+		y = event.clientY - 20
+		if @lines > @height
+			top = y / @height * $(".CodeMirror").height() | 0
+		else
+			top = @editor.charCoords(
+				line: Math.max(0, y)
+				ch: 0
+			).y
+		$(window).scrollTop top
+
+	resize: =>
+		if ($(window).height() > @lines)
+			@height = $(window).height()
+			@canvas = $("#scroll").attr("height", @height)
+			@context = @canvas[0].getContext("2d")
+			@frame = {@data} = @context.createImageData(@width, @height)
+		@bottom = @editor.coordsChar(x: null, y: $(window).scrollTop() + $(window).height()).line + 1

 	downloadAs: (name) ->
 		raw = @editor.getValue()
@@ -183,4 +191,21 @@
 				, ->)
 			, ->)
 		, (e) -> console.error(e))
+
+	charAlpha: (ch) ->
+		if /[._'"\-]/.test(ch)
+			150
+		else if /[flyg]/.test(ch)
+			225
+		else if /[a-z]/.test(ch)
+			200
+		else
+			255
+
+	setPixel: (x, y, r, g, b, a) ->
+		index = (x + y * @width) * 4
+		@data[index + 0] = r
+		@data[index + 1] = g
+		@data[index + 2] = b
+		@data[index + 3] = a


Fixing extraction regexp, and removing it's found contents from `@raw`.

--- src/cli/toaster/core/script.coffee
+++ src/cli/toaster/core/script.coffee
@@ -145,9 +145,10 @@
     deps_args = deps_args.slice 0, -1

     # filter code that must to be outside of the 'define' block
-    global_reg = XRegExp('(#>>)\n(.*)\n#<<', 's')
+    global_reg = XRegExp('#>>\n(.*)\n#<<', 's')
     global_res = XRegExp.exec @backup, global_reg
     global_code = if global_res? then global_res[1] else ''
+    @raw = @raw.replace global_code, ''

     # detect file identation style..
     match_identation = /^(\s+).*$/mg


Fix bug where you can't edit account settings while checking for achievement updates

--- app/models/User.coffee
+++ app/models/User.coffee
@@ -383,7 +383,14 @@
   checkForNewAchievement: (options={}) ->
     options.url = _.result(@, 'url') + '/check-for-new-achievement'
     options.type = 'POST'
-    @fetch(options)
+    jqxhr = @fetch(options)
+
+    # Setting @loading to false because otherwise, if the user tries to edit their settings while checking
+    # for new achievements, the changes won't be saved. This is because AccountSettingsView relies on
+    # hasLocalChanges, and that is only true if, when set is called, the model isn't "loading".
+    @loading = false
+
+    return jqxhr

 tiersByLevel = [-1, 0, 0.05, 0.14, 0.18, 0.32, 0.41, 0.5, 0.64, 0.82, 0.91, 1.04, 1.22, 1.35, 1.48, 1.65, 1.78, 1.96, 2.1, 2.24, 2.38, 2.55, 2.69, 2.86, 3.03, 3.16, 3.29, 3.42, 3.58, 3.74, 3.89, 4.04, 4.19, 4.32, 4.47, 4.64, 4.79, 4.96,
   5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 10, 10.5, 11, 11.5, 12, 12.5, 13, 13.5, 14, 14.5, 15


Limit initial repos request to 20 repos

Limit initial repos request to 20 repos

--- app/models/repo.coffee
+++ app/models/repo.coffee
@@ -132,7 +132,7 @@
     )

     promise = new Ember.RSVP.Promise (resolve, reject) ->
-      store.query('repo', { 'repository.active': 'true' }).then( ->
+      store.query('repo', { 'repository.active': 'true', limit: 20 }).then( ->
         resolve(repos)
       , ->
         reject()


Use underscore node module

Use underscore node module

--- lib/bracket-matcher.coffee
+++ lib/bracket-matcher.coffee
@@ -1,4 +1,4 @@
-_ = require 'underscore'
+_ = nodeRequire 'underscore'
 {$$} = require 'space-pen'
 Range = require 'range'


Bugfix in inline \includegraphics

Bugfix in inline \includegraphics

There was a missing } in the generated \includegraphics statement.

--- client/import.iced
+++ client/import.iced
@@ -482,7 +482,7 @@
             match
         .replace /\\includegraphics\s*(\[[^\[\]]*\]\s*)?{((?:[^{}]|{[^{}]*})*)}/g,
           (match, optional, filename) ->
-            "\\includegraphics#{optional}{coauthor:#{inlineGraphics[filename].message._id}"
+            "\\includegraphics#{optional}{coauthor:#{inlineGraphics[filename].message._id}}"
         ## Now we can upload the message itself.
         revision = _.clone message
         revision.updated = revision.created


Added TODO.

Added TODO.

--- client/main.coffee
+++ client/main.coffee
@@ -18,6 +18,7 @@
       Session.set 'currentSearchQuery', parseQuery this.querystring
       'results'

+  # TODO: Use real parser (arguments can be listed multiple times, arguments can be delimited by ";")
   parseQuery = (qs) ->
     query = {}
     for pair in qs.replace('?', '').split '&'


Move iterator state to instance object so they don't all share the same state

--- src/coffee/leveldb/iterator.coffee
+++ src/coffee/leveldb/iterator.coffee
@@ -28,41 +28,43 @@

 exports.Iterator = class Iterator

-  busy = false
-  isValid = false
-  keybuf = null
-  valbuf = null
-
-  lock = ->
-    throw new Error 'Concurrent operations not supported' if busy
-    busy = true
-
-  unlock = ->
-    throw new Error 'Not locked' unless busy
-    busy = false
-
-  afterSeek = (callback) -> (err, valid, key, val) ->
-    unlock()
-    isValid = valid
-    keybuf = key
-    valbuf = val
-    callback err if callback
-
-  getKey = (options = {}) ->
-    unless keybuf
-      return null
-    else unless options.as_buffer
-      keybuf.toString 'utf8'
+  toBuffer = (val) ->
+    new Buffer val unless Buffer.isBuffer val
+
+  toValue = (val, options) ->
+    unless val
+      null
+    else unless options?.as_buffer
+      val.toString 'utf8'
     else
-      keybuf
-
-  getValue = (options = {}) ->
-    unless valbuf
-      return null
-    else unless options.as_buffer
-      valbuf.toString 'utf8'
-    else
-      valbuf
+      val
+
+  wrapSeek = (callback, validate) ->
+    @_lock()
+
+    throw new Error 'Illegal state' if validate and not @_valid
+    throw new Error 'Missing callback' unless callback
+
+    (err, valid, key, val) =>
+      @_unlock()
+      @_valid = valid
+      @_key = key
+      @_val = val
+      callback err if callback
+
+  _lock: ->
+    throw new Error 'Concurrent operations not supported' if @_busy
+    @_busy = true
+
+  _unlock: ->
+    throw new Error 'Not locked' unless @_busy
+    @_busy = false
+
+  _getKey: (options) ->
+    toValue @_key, options
+
+  _getVal: (options) ->
+    toValue @_val, options

   ###
@@ -74,6 +76,9 @@
   ###

   constructor: (@self) ->
+    @_busy = @_valid = false
+    @_key = @_val = null
+    @_wrapSeek = wrapSeek.bind @

   ###
@@ -118,15 +123,18 @@
     # optional keys
     [ startKey, limitKey ] = args

+    # for comparing end key
     limit = limitKey.toString 'binary' if limitKey

+    # loop function
     next = (err) =>
       return callback err if err
-      if isValid
-        callback null, getKey(options), getValue(options)
-        if not limit or limit isnt keybuf.toString 'binary'
+      if @_valid
+        callback null, @_getKey(options), @_getVal(options)
+        if not limit or limit isnt @_key.toString 'binary'
           @next next

+    # start loop
     if startKey
       @seek startKey, next
     else
@@ -139,7 +147,7 @@

   ###

-  valid: -> isValid
+  valid: -> @_valid

   ###
@@ -153,9 +161,7 @@
   ###

   seek: (key, callback) ->
-    key = new Buffer key unless Buffer.isBuffer key
-    lock()
-    @self.seek key, afterSeek callback
+    @self.seek toBuffer(key), @_wrapSeek callback

   ###
@@ -168,8 +174,7 @@
   ###

   first: (callback) ->
-    lock()
-    @self.first afterSeek callback
+    @self.first @_wrapSeek callback

   ###
@@ -182,8 +187,7 @@
   ###

   last: (callback) ->
-    lock()
-    @self.last afterSeek callback
+    @self.last @_wrapSeek callback

   ###
@@ -196,9 +200,7 @@
   ###

   next: (callback) ->
-    throw new Error 'Illegal state' unless isValid
-    lock()
-    @self.next afterSeek callback
+    @self.next @_wrapSeek callback, true

   ###
@@ -211,9 +213,7 @@
   ###

   prev: (callback) ->
-    throw new Error 'Illegal state' unless isValid
-    lock()
-    @self.prev afterSeek callback
+    @self.prev @_wrapSeek callback, true

   ###
@@ -230,16 +230,15 @@
   ###

   key: (options, callback) ->
-    throw new Error 'Illegal state' unless isValid

     # optional options
     if typeof options is 'function'
       callback = options
       options = null

-    throw new Error 'Missing callback' unless callback
-
-    callback null, getKey options
+    key = @_getKey options
+    callback? null, key
+    key

   ###
@@ -262,7 +261,9 @@
       callback = options
       options = null

-    callback null, getValue options
+    val = @_getVal options
+    callback? null, val
+    val

   ###
@@ -286,6 +287,7 @@
       callback = options
       options = null

-    throw new Error 'Missing callback' unless callback
-
-    callback null, getKey(options), getValue(options)
+    key = @_getKey options
+    val = @_getVal options
+    callback? null, key, val
+    [key, val]


link of skype added to white list of safe links

--- app/scripts/app.coffee
+++ app/scripts/app.coffee
@@ -2,6 +2,6 @@

 angular.module('whoruApp', ['ngRoute','ngSanitize'])
   .config(['$compileProvider', ($compileProvider) ->
-    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|file|tel):/)
+    $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|ftp|mailto|file|tel|skype):/)
   ])


allow pinned prop for multiparts

allow pinned prop for multiparts

--- src/components/exercise/index.cjsx
+++ src/components/exercise/index.cjsx
@@ -123,7 +123,7 @@
     </ScrollTracker>

   render: ->
-    {parts, footer} = @props
+    {parts, footer, pinned} = @props

     if @isSinglePart()
       return @renderSinglePart()
@@ -133,7 +133,7 @@
     exerciseGroup = @renderGroup()
     footer ?= @renderFooter()

-    <CardBody footer={footer} className='openstax-multipart-exercise-card'>
+    <CardBody footer={footer} pinned={pinned} className='openstax-multipart-exercise-card'>
       <ExerciseBadges isMultipart={true}/>
       {exercisePartsWithScroll}
       {exerciseGroup}
@@ -144,7 +144,7 @@
   displayName: 'Exercise'
   mixins: [ExerciseMixin]
   render: ->
-    {footer} = @props
+    {footer, pinned} = @props

     if @isSinglePart()
       return @renderSinglePart()
@@ -153,7 +153,7 @@
     exerciseGroup = @renderGroup()
     footer ?= @renderFooter()

-    <CardBody footer={footer} className='openstax-multipart-exercise-card'>
+    <CardBody footer={footer} pinned={pinned} className='openstax-multipart-exercise-card'>
       <ExerciseBadges isMultipart={true}/>
       {exerciseParts}
       {exerciseGroup}


update task review with quick :memo:

update task review with quick :memo:

--- src/components/task-step/ends.cjsx
+++ src/components/task-step/ends.cjsx
@@ -10,7 +10,7 @@

 TaskStep = require './index'
 {TaskStore} = require '../../flux/task'
-{TaskStepStore, TaskStepActions} = require '../../flux/task-step'
+{TaskStepStore} = require '../../flux/task-step'

 PracticeEnd = React.createClass
   displayName: 'PracticeEnd'
@@ -52,6 +52,7 @@
   bindStore: TaskStepStore
   bindEvent: 'step.completed'
   bindUpdate: ->
+    # update on complete
     @setState({})

   propTypes:


Updates initialization

Updates initialization

--- apps/rss/routes.coffee
+++ apps/rss/routes.coffee
@@ -9,7 +9,7 @@

 @channelRSS = (req, res, next) ->
   channel = new Channel
-    channel_slug: req.params.channel_slug
+    id: req.params.channel_slug

   blocks = new ChannelBlocks null,
     channel_slug: req.params.channel_slug


selection box

selection box

--- coffee/main.coffee
+++ coffee/main.coffee
@@ -86,6 +86,60 @@
       @obj3d.lines = []

     select: ->
+
+  SelectionCube: class SelectionCube
+    constructor: () ->
+      @geom = new THREE.Geometry()
+      @geom.dynamic = true
+
+      @aa = new THREE.Vector3(0, 0, 75)
+      @ab = new THREE.Vector3(0, 0, 75)
+      @ba = new THREE.Vector3(0, 0, 75)
+      @bb = new THREE.Vector3(0, 0, 75)
+
+      @geom.vertices.push(
+        @aa, @ab, @ba,
+        @bb, @ba, @ab
+      )
+
+      @geom.faces.push(
+        new THREE.Face3(0, 1, 2),
+        new THREE.Face3(2, 1, 0),
+        new THREE.Face3(3, 4, 5),
+        new THREE.Face3(5, 4, 3)
+      )
+
+      @geom.computeBoundingSphere()
+      @material = new THREE.MeshBasicMaterial({color: 0x123456})
+      @obj3d = new THREE.Mesh(@geom, @material)
+
+    updateGFX: () ->
+      @geom.verticesNeedUpdate = true
+      @geom.lineDistancesNeedUpdate = true
+      @geom.elementsNeedUpdate = true
+      @geom.normalsNeedUpdate = true
+      @geom.computeFaceNormals()
+      @geom.computeVertexNormals()
+      @geom.computeBoundingSphere()
+      @geom.computeBoundingBox()
+
+    init: (p) ->
+      @aa.x = @ab.x = @ba.x = p.x
+      @aa.y = @ba.y = @ab.y = p.y
+      @bb.x = @aa.x
+      @bb.y = @aa.y
+      @updateGFX()
+
+
+    update: (p) ->
+      @bb.x = @ba.x = p.x
+      @bb.y = @ab.y = p.y
+      @updateGFX()
+
+    reset: () ->
+      @aa.x = @bb.x = @ba.x = @ab.x = 0
+      @aa.y = @bb.y = @ba.y = @ab.y = 0
+      @updateGFX()

 }

@@ -294,6 +348,8 @@
     @ve.scene.add(@baseRect.obj3d)
     @selectGroup = new THREE.Group()
     @ve.scene.add(@selectGroup)
+    @selectorGroup = new THREE.Group()
+    @ve.scene.add(@selectorGroup)
     @elements = []

   addElement: (ef, x, y) ->
@@ -457,6 +513,10 @@
     @clearPropsGUI()
     @ve.render()

+  clearSelector: ->
+    @selectorGroup.children = []
+    @ve.render()
+
 class NameManager
   constructor: () ->
     @names = new Array()
@@ -680,14 +740,45 @@

 class SurfaceSpaceSelectHandler
   constructor: (@mh) ->
+    @selCube = new SelectionCube()

   test: (ixs) ->
     ixs.length > 0 and
     ixs[0].object.userData instanceof Surface

   handleDown: (ixs) ->
-    console.log "! surface clear " + ixs.length
+    console.log "! surface select down"
+    p = new THREE.Vector3(
+      ixs[ixs.length - 1].point.x,
+      ixs[ixs.length - 1].point.y,
+      75
+    )
+    @selCube.init(p)
+    @mh.ve.surface.selectorGroup.add(@selCube.obj3d)
+    @mh.ve.container.onmouseup = (eve) => @handleUp(eve)
+    @mh.ve.container.onmousemove = (eve) => @handleMove(eve)
     @mh.ve.surface.clearSelection()
+
+  handleUp: (event) ->
+    console.log "! surface select up"
+    @selCube.reset()
+    @mh.ve.container.onmousemove = null
+    @mh.ve.container.onmousedown = (eve) => @mh.baseDown(eve)
+    @mh.ve.container.onmouseup = null
+    @mh.ve.surface.clearSelector()
+    @mh.ve.render()
+
+  handleMove: (event) ->
+    bix = @mh.baseRectIx(event)
+    if bix.length > 0
+      p = new THREE.Vector3(
+        bix[bix.length - 1].point.x,
+        bix[bix.length - 1].point.y,
+        75
+      )
+      @selCube.update(p)
+      @mh.ve.render()
+

 class LinkingHandler
   constructor: (@mh) ->
@@ -750,6 +841,7 @@
     #console.log "! lm0"

   handleMove1: (event) ->
+    #TODO replace me with baseRectIx when that is ready
     @.mh.updateMouse(event)
     @.mh.ve.raycaster.setFromCamera(@.mh.pos, @.mh.ve.camera)
     bix = @.mh.ve.raycaster.intersectObject(@.mh.ve.surface.baseRect.obj3d)
@@ -777,6 +869,11 @@
     @pos.x =  (event.layerX / @ve.container.offsetWidth ) * 2 - 1
     @pos.y = -(event.layerY / @ve.container.offsetHeight) * 2 + 1
     #console.log(@pos.x + "," + @pos.y)
+
+  baseRectIx: (event) ->
+    @updateMouse(event)
+    @ve.raycaster.setFromCamera(@pos, @ve.camera)
+    @ve.raycaster.intersectObject(@ve.surface.baseRect.obj3d)

   placingObject: null
   placingLink: null


Style fixes for disable view cache.

Style fixes for disable view cache.

--- src/controller/render_cache.coffee
+++ src/controller/render_cache.coffee
@@ -7,11 +7,11 @@
     @keyQueue = []

   viewForOptions: (options) ->
-    if Batman.config.cacheViews || options.cache == true || options.viewClass::cache == true
+    if Batman.config.cacheViews || options.cache || options.viewClass::cache
       @getOrSet options, =>
         @_newViewFromOptions(Batman.extend {}, options)
     else
-      return @_newViewFromOptions(options)
+      @_newViewFromOptions(options)

   _newViewFromOptions: (options) -> new options.viewClass(options)


CLEANUP ALL 64 PASSING

CLEANUP
ALL 64 PASSING

--- source/BabelBridge/Nodes/Node.coffee
+++ source/BabelBridge/Nodes/Node.coffee
@@ -8,33 +8,27 @@
 Stats = require '../Stats'

 module.exports = class Node extends BaseClass
-  constructor: (@_parent, options) ->
+  constructor: (parent, options) ->
     super
     Stats.add "newNode"
-    {@_parser} = @_parent
+    @_parent = parent
+    {@_parser} = parent

     @_offset = (options?.offset ? @_parent.getNextOffset()) | 0
     @_matchLength = 0
+
+    @_ruleName = @_pluralRuleName =
+    @_label = @_pluralLabel = @_pattern = @_nonMatches =
     @_ruleVariant = @_matches = @_matchPatterns = null

+    @_labelsApplied =
+    @_nonMatch = false
+
     if options
-      @_matchLength = (options.matchLength || 0) | 0
-      @_ruleVariant = options.ruleVariant
-      @_matches = options.matches
+      @_matchLength   = (options.matchLength || 0) | 0
+      @_ruleVariant   = options.ruleVariant
+      @_matches       = options.matches
       @_matchPatterns = options.matchPatterns
-
-    # {@offset, @matchLength, @ruleVariant, @matches, @matchPatterns} = options if options
-    # @_offset ?= @_parent.getNextOffset()
-    # @_matchLength ||= 0
-    @_labelsApplied = false
-
-    @_ruleName = null
-    @_pluralRuleName = null
-    @_label = null
-    @_pluralLabel = null
-    @_pattern = null
-    @_nonMatch = false
-    @_nonMatches = null

   # provided so CaffineScript or other ES6-class-based systems can define their own class extension
   @_createSubclassBase: ->


mood can not go below 0

mood can not go below 0

--- assets/customer.coffee
+++ assets/customer.coffee
@@ -15,10 +15,12 @@

   reduceMood: (multiplier = 1) ->
     @mood -= (0.05 * multiplier * @volatility)
+    if @mood < 0
+      @mood = 0 # TODO - remove customer?

   increaseMood: (multiplier = 1) ->
     if(@mood < 1)
-      @mood += (0.05 * multiplier)
+      @mood += (0.05 * multiplier * @volatility)
       if(@mood > 1)
         @mood = 1


feat (DrawerCollection) add drawer enable/disable API support

--- src/models/DrawerCollection.coffee
+++ src/models/DrawerCollection.coffee
@@ -23,6 +23,26 @@
         parallaxFactor: 2.0
       })
     }
+
+  enable: (options={}, callbacks={}) ->
+    parameters = {}
+    parameters.side = options.side
+
+    steroids.nativeBridge.nativeCall
+      method: "enableDrawer"
+      parameters: parameters
+      successCallbacks: [callbacks.onSuccess]
+      failureCallbacks: [callbacks.onFailure]
+
+  disable: (options={}, callbacks={}) ->
+    parameters = {}
+    parameters.side = options.side
+
+    steroids.nativeBridge.nativeCall
+      method: "disableDrawer"
+      parameters: parameters
+      successCallbacks: [callbacks.onSuccess]
+      failureCallbacks: [callbacks.onFailure]

   hide: (options={}, callbacks={}) ->
     steroids.debug "steroids.drawers.hide called"


Add Function and RegExp to recognized js types.

--- src/finitio/support/factory.coffee
+++ src/finitio/support/factory.coffee
@@ -62,10 +62,12 @@

   constructor: (world)->
     @world = {
-      'Number':  Number,
-      'String':  String,
-      'Boolean': Boolean,
-      'Date':    Date
+      'Number':   Number,
+      'String':   String,
+      'Boolean':  Boolean,
+      'Date':     Date,
+      'Function': Function,
+      'RegExp':   RegExp
     }
     $u.extend(@world, world)


atom: setup python black

atom: setup python black

--- atom/.atom/config.cson
+++ atom/.atom/config.cson
@@ -22,6 +22,7 @@
   editor:
     fontFamily: "Iosevka Light"
     fontSize: 12
+    preferredLineLength: 88
     showIndentGuide: true
     showInvisibles: true
   "exception-reporting":
@@ -30,6 +31,11 @@
     onChanges: true
   "ide-flowtype":
     tryFlowBin: true
+  "ide-python":
+    pylsPlugins:
+      pycodestyle:
+        maxLineLength: 88
+    python: "/usr/bin/python"
   minimap:
     plugins:
       "git-diff": true
@@ -72,6 +78,9 @@
       enabled: true
       isDisabledIfNoConfigFile: true
       showInStatusBar: true
+  "python-black":
+    binPath: "/home/sun/.local/bin/black"
+    showErrors: "flash"
   "tool-bar":
     visible: false
   "tree-view":


Use relative path for task handler

Use relative path for task handler

--- lib/spell-check-view.coffee
+++ lib/spell-check-view.coffee
@@ -12,7 +12,7 @@
   views: []

   initialize: (@editor) ->
-    @task = new Task('spell-check/lib/spell-check-handler')
+    @task = new Task(require.resolve('./spell-check-handler'))
     @subscribe @editor, 'editor:path-changed', @subscribeToBuffer
     @subscribe @editor, 'editor:grammar-changed', @subscribeToBuffer
     @observeConfig 'editor.fontSize', @subscribeToBuffer


make the anonymous "Sign Up!" button a big more obnoxiously big... tired of people missing it.

--- src/smc-webapp/desktop_app.cjsx
+++ src/smc-webapp/desktop_app.cjsx
@@ -140,7 +140,7 @@
             a = 'cog'

         if @props.is_anonymous
-            label = <Button bsStyle="success" style={fontWeight:'bold'}>Sign Up!</Button>
+            label = <Button bsStyle="success" style={fontWeight:'bold', width:'400px'}>Sign Up!</Button>
             style = {marginTop:'-10px'}  # compensate for using a button
         else
             label = "Account"


(fix) replaced Q with Promise

(fix) replaced Q with Promise

--- src/steroids/SafariDebug.coffee
+++ src/steroids/SafariDebug.coffee
@@ -6,8 +6,6 @@
 Help = require "./Help"

 os = require "os"
-
-Q = require "q"

 class SafariDebug

@@ -43,7 +41,6 @@
     @ensureAssistiveAccess().then( =>
       scriptPath = path.join paths.scriptsDir, scriptFileName

-
       args = if argument?
         [scriptPath].concat argument
       else
@@ -68,7 +65,7 @@

         @callBackOnExit?()

-    ).fail (errMsg) =>
+    ).catch (errMsg) =>
       console.error chalk.red errMsg
       @callBackOnExit?()

@@ -99,28 +96,25 @@

         @callBackOnExit?()

-    ).fail (errMsg) =>
+    ).catch (errMsg) =>
       console.error chalk.red errMsg
       @callBackOnExit?()

   ensureAssistiveAccess: =>
-    deferred = Q.defer()
+    new Promise (resolve, reject) ->
+      scriptPath = path.join paths.scriptsDir, "ensureAssistiveAccess.scpt"

-    scriptPath = path.join paths.scriptsDir, "ensureAssistiveAccess.scpt"
+      ensureAssistiveAccessSbawn = sbawn
+        cmd: "osascript"
+        args: [scriptPath]

-    ensureAssistiveAccessSbawn = sbawn
-      cmd: "osascript"
-      args: [scriptPath]
+      ensureAssistiveAccessSbawn.on "exit", () =>
+        steroidsCli.debug "Ensure assistive access started and killed"

-    ensureAssistiveAccessSbawn.on "exit", () =>
-      steroidsCli.debug "Ensure assistive access started and killed"
-
-      if ensureAssistiveAccessSbawn.code
-        errMsg = '\nERROR: ' + (/\ execution error: ([\s\S]+)$/.exec(ensureAssistiveAccessSbawn.stderr)?[1] || ensureAssistiveAccessSbawn.stderr)
-        deferred.reject(errMsg)
-      else
-        deferred.resolve()
-
-    deferred.promise
+        if ensureAssistiveAccessSbawn.code
+          errMsg = '\nERROR: ' + (/\ execution error: ([\s\S]+)$/.exec(ensureAssistiveAccessSbawn.stderr)?[1] || ensureAssistiveAccessSbawn.stderr)
+          reject errMsg
+        else
+          resolve()

 module.exports = SafariDebug


Fix spec linting

Fix spec linting

--- spec/models/file-summary-spec.coffee
+++ spec/models/file-summary-spec.coffee
@@ -9,12 +9,12 @@

   it "stages if status is unstaged", ->
     @fileSummary.status = 'unstaged'
-    spyOn(@fileSummary.gitIndex, 'stagePath').andReturn(new Promise ->)
+    spyOn(@fileSummary.gitIndex, 'stagePath').andReturn(new Promise -> )
     @fileSummary.stage()
     expect(@fileSummary.gitIndex.stagePath).toHaveBeenCalled()

   it "unstages if status is not unstaged", ->
     @fileSummary.status = 'staged'
-    spyOn(@fileSummary.gitIndex, 'unstagePath').andReturn(new Promise ->)
+    spyOn(@fileSummary.gitIndex, 'unstagePath').andReturn(new Promise -> )
     @fileSummary.stage()
     expect(@fileSummary.gitIndex.unstagePath).toHaveBeenCalled()


Add keypath functions

Add keypath functions

--- framer/Utils.coffee
+++ framer/Utils.coffee
@@ -9,6 +9,18 @@
 Utils.getValue = (value) ->
 	return value() if _.isFunction value
 	return value
+
+Utils.getValueForKeyPath = (obj, key) ->
+	result = obj
+	for key in key.split(".")
+		result = result[key]
+	return result
+
+Utils.setValueForKeyPath = (obj, key, value) ->
+	parts = key.split(".")
+	propertyName = _.last(parts)
+	key = parts[0..parts.length-2].join(".")
+	Utils.getValueForKeyPath(key)[propertyName] = value

 Utils.setDefaultProperties = (obj, defaults, warn=true) ->


fix npe

fix npe

--- app/assets/javascripts/states/monitoring/views/metric_detail.js.coffee
+++ app/assets/javascripts/states/monitoring/views/metric_detail.js.coffee
@@ -99,8 +99,9 @@
           view.$("div.zoom_level button .text").text("Custom")

       @graph = Bixby.monitoring.render_metric(@$("div.metric"), @metric, {}, zoom_callback)
-      @sync_helper = new Bixby.monitoring.PanSyncHelper(@graph)
-      @graph._bixby_touch_enabled = true # enable touch events in detailed view
+      if @graph
+        @sync_helper = new Bixby.monitoring.PanSyncHelper(@graph)
+        @graph._bixby_touch_enabled = true # enable touch events in detailed view

       if @level
         # make sure we show the entire date range


Fix key combo

Fix key combo

--- macros.coffee
+++ macros.coffee
@@ -12,7 +12,7 @@

 @helloConsole = ->
   console.log 'Hello console'
-  alert('Watch your console! (open with alt-cmd-i)')
+  alert('Watch your console! (open with shift-ctrl-i)')

 @helloConsole.icon = 'ion-clipboard'
 # icon from https://atom.io/packages/toolbar#supported-icon-sets


add: comment of recommendation to use json2.js for IE

--- jquery.store.coffee
+++ jquery.store.coffee
@@ -1,7 +1,12 @@
 # jQuery Store
 #
 # jQuery Store can store almost all type of objects.
-# JSON.parse and JSON.stringify used
+#
+# In the codes, JSON.parse and JSON.stringify used.
+# I recommend to insert this code in your html:
+# <!--[if lt IE 8]>
+# <script src="./JSON-js/json2.js"></script>
+# <![endif]-->

 (($) ->
   $.fn.extend


webapp/notification bell: fixing right margin (did underlap with the network info!) and its hover state

--- src/smc-webapp/app_shared.cjsx
+++ src/smc-webapp/app_shared.cjsx
@@ -116,13 +116,11 @@
     render: ->
         outer_styles =
             position    : 'relative'
-            marginRight : '-10px'
             float       : 'left'

         inner_styles =
             padding  : '10px'
             fontSize : '17pt'
-            color    : '#666'
             cursor   : 'pointer'

         clz = ''


CADS-1181 : Circs - BIC: Question "What date did you decide that this break was permanent?" not shown if error message displayed when same Q. not completed.

--- c3/app/assets/javascripts/circs/report_changes/breaksFromCaring.coffee
+++ c3/app/assets/javascripts/circs/report_changes/breaksFromCaring.coffee
@@ -15,10 +15,17 @@

   if not $("#" + expectStartCaringY).prop("checked")
     hideExpectStartCaringDate()
-    hidePermanentBreakDate()

   if not $("#" + expectStartCaringN).prop("checked")
     hidePermanentBreakDate()
+
+  if $("#" + expectStartCaringY).prop("checked")
+    showExpectStartCaringDate()
+    hidePermanentBreakDate()
+
+  if $("#" + expectStartCaringN).prop("checked")
+    showPermanentBreakDate()
+    hideExpectStartCaringDate()

   if isNotMondayOrFriday(breakStartDate)
     hideTime(breakStartTime)


Showing syntax erros properly.

Showing syntax erros properly.

--- src/toaster/core/script.coffee
+++ src/toaster/core/script.coffee
@@ -197,6 +197,17 @@

   # compile file and returns it as string
   compile_to_str:( add_definitions = false )->
+
+    try
+      cs.compile @backup
+    catch err
+      # catches and shows it, and abort the compilation
+      msg = err.message.replace '"', '\\"'
+      msg = "#{msg.white} @ " + "#{@filepath}".bold.red
+      # console.log @raw
+      error msg
+      return null
+
     raw = if add_definitions then @defined_raw else @raw
     compiled = cs.compile raw, bare: @builder.config.bare


Towards images in subject viewer

Towards images in subject viewer

--- src/ubret/subject_viewer.coffee
+++ src/ubret/subject_viewer.coffee
@@ -11,12 +11,12 @@

   start: =>
     subjects = new Array
-    if @selectedElements.length isnt 0
+    if typeof @selectedElements isnt 'undefined' and @selectedElements.length isnt 0
       subjects = @dimensions.uid.top(Infinity).filter (item) =>
         item.uid in @selectedElements
     else
       subjects = [@dimensions.uid.top(1)[0]]
-      @selectElements subjects
+      @selectElements(_.pluck subjects, 'uid')
     @render(subjects)

   render: (subjects) =>
@@ -27,9 +27,8 @@
         .append('div')
         .attr('class', 'subject')

-    subject.selectAll('img')
-      .append('img')
-        .attr('src', (d) -> d.image)
+    subject.append('img')
+        .attr('src', (d) -> console.log 'here'; d.image)

     subject.selectAll('ul')
       .append('ul')


MaxLength error timeout to warning instead of "ok".

--- src/validator/MaxLength.coffee
+++ src/validator/MaxLength.coffee
@@ -65,7 +65,7 @@
     if v.length > @max
       @_error()
       @_errorTimeout = setTimeout =>
-        @_ok()
+        if @warn < @max then @_warning() else @_ok()
       , @removeTime
     else if v.length > @warn
       @_warning()


Client: Activity: Feed: Added SocialMediaWidget component to sidebar

--- client/activity/lib/components/publicfeedpane/feedthreadsidebar.coffee
+++ client/activity/lib/components/publicfeedpane/feedthreadsidebar.coffee
@@ -3,14 +3,18 @@
 PostingGuideLinesWidget    = require 'activity/components/sidebarwidgets/postingguidelineswidget'
 ActivityAnnouncementWidget = require 'activity/components/sidebarwidgets/activityannouncementwidget'
 MostReadArticlesWidget     = require 'activity/components/sidebarwidgets/mostreadarticleswidget'
+SocialMediaWidget          = require 'activity/components/sidebarwidgets/socialmediawidget'
+

 module.exports = class FeedThreadSidebar extends React.Component

   render: ->
+
     <Scroller className="FeedThreadSidebar">
       <ActivityAnnouncementWidget />
       <PostingGuideLinesWidget />
       <MostReadArticlesWidget />
+      <SocialMediaWidget />
     </Scroller>


webapp/latex/Rnw: using concordance information to map latex errors to the lines in the Rnw file

--- src/smc-webapp/editor_latex.coffee
+++ src/smc-webapp/editor_latex.coffee
@@ -39,6 +39,8 @@
         @element = templates.find(".salvus-editor-latex").clone()

         @_pages = {}
+
+        # this maps tex line numbers to the ones in the Rnw file via an array
         @_rnw_concordance = null

         # initialize the latex_editor
@@ -547,10 +549,7 @@
                 log_output.text(log_output.text() + '\n\n-----------------------------------------------------\nRunning ' + mesg.start + '...\n\n\n\n')
             else
                 if mesg.end == 'latex'
-                    async.series([
-                        @get_rnw_concordance,
-                        @render_error_page
-                    ])
+                    @render_error_page()
                 build_status.text('')
                 log_output.text(log_output.text() + '\n' + mesg.log + '\n')
             # Scroll to the bottom of the textarea
@@ -564,7 +563,14 @@
                 @_show() # update layout, since hiding spinner might cause a linebreak in the button bar to go away
                 opts.cb?()

-    render_error_page: (cb) =>
+    render_error_page: () =>
+        # looks bad, but it isn't: @_render_error_page is synchronized at it's heart
+        async.series([
+            @get_rnw_concordance,
+            @_render_error_page
+        ])
+
+    _render_error_page: (cb) =>
         log = @preview.pdflatex.last_latex_log
         if not log?
             cb()
@@ -573,10 +579,10 @@

         if p.errors.length
             @number_of_errors.text("  (#{p.errors.length})")
-            @element.find("a[href=\"#errors\"]").addClass("btn-danger")
+            @element.find('a[href="#errors"]').addClass("btn-danger")
         else
             @number_of_errors.text('')
-            @element.find("a[href=\"#errors\"]").removeClass("btn-danger")
+            @element.find('a[href="#errors"]').removeClass("btn-danger")

         k = p.warnings.length + p.typesetting.length
         if k and p.errors.length == 0  # don't show if there are errors
@@ -633,7 +639,7 @@
         if not file
             alert_message
                 type    : "error"
-                message : "No way to open unknown file."
+                message : "Unable to open unknown file."
             cb?()
             return
         if not mesg.line
@@ -678,8 +684,9 @@
         if mesg.file?.slice(0,2) == './'
             mesg.file = mesg.file.slice(2)

-        if mesg.line and mesg.file == preview.pdflatex.filename_rnw
+        if mesg.line and @preview.pdflatex.ext == 'rnw'
             mesg.line = @rnw_concordance(mesg.line)
+            mesg.file = @preview.pdflatex.filename_rnw

         elt = @_error_message_template.clone().show()
         elt.find("a:first").click () =>
@@ -705,9 +712,14 @@
     # convert line number of tex file to line number in Rnw file
     rnw_concordance: (line) =>
         if not @_rnw_concordance?
+            # TODO linear interpolation using line number of tex vs. @latex_editor.lineCount()
             return line
+        ret = @_rnw_concordance[line - 1] ? line
+        # console.log("associated line of #{line} is #{ret}")
+        return ret

     get_rnw_concordance: (cb) =>
+        # always call the cb without an error -- otherwise the errors don't show up at all
         if @preview.pdflatex.ext == 'rnw'
             conc_fn = @preview.pdflatex.base_filename + '-concordance.tex'
             salvus_client.read_text_file_from_project
@@ -718,12 +730,29 @@
                     if err
                         alert_message
                             type    : "error"
-                            message : "Unable to read concordance file -- #{err}"
+                            message : "Unable to read concordance file #{conc_fn} -- #{err}"
                     else
-                        c = res.content
-                        c = c.split('%')[1..].join(' ').replace(/\n/g, '')
-                        c = c[...c.indexOf('}')]
-                        @_rnw_concordance = (parseInt(n) for n in c.split(/[ ]+/))
+                        # concordance file is explained here:
+                        # https://cran.r-project.org/web/packages/patchDVI/vignettes/patchDVI.pdf
+                        try
+                            c = res.content
+                            c = c.split('%')[1..].join(' ').replace(/\n/g, '')
+                            c = c[...c.indexOf('}')]
+                            enc  = (parseInt(n) for n in c.split(/[ ]+/))
+                            # enc is now the list of RLE encoded numbers
+                            line = enc[0] - 1 # start line, zero-based
+                            orig = 0
+                            dec  = []     # decoded RLE encoding
+                            for idx in [1...enc.length] by 2
+                                for i in [0...enc[idx]]
+                                    orig += enc[idx + 1]
+                                    dec.push(orig)
+                            # console.log('@_rnw_concordance', @_rnw_concordance)
+                            @_rnw_concordance = dec
+                        catch e
+                            # don't reset @_rnw_concordance, the old one could be good enough
+                            console.warn("problem reading and processing #{conc_fn}:", e)
+                            console.trace()
                     cb()
         else
             cb()


shoud be a string

shoud be a string

--- lib/services/ContentDirectory.coffee
+++ lib/services/ContentDirectory.coffee
@@ -137,8 +137,8 @@
       # Increment each time container (or parent container) is modified.
       @redis.incr "#{if object.type is 'container' then id else parentID}:updateid"
       # Add ID's to item data structure and insert into data store.
-      object.id = id
-      object.parentid = parentID
+      object.id = "#{id}"
+      object.parentid = "#{parentID}"
       @redis.hmset "#{id}", object
       cb err, id


Use `.classList.toggle()`

Use `.classList.toggle()`

Instead of `.classList[method]()`. Much nicer.

--- extension/packages/mode-hints/marker.coffee
+++ extension/packages/mode-hints/marker.coffee
@@ -24,8 +24,9 @@
   show: -> @setVisibility(true)
   hide: -> @setVisibility(false)
   setVisibility: (visible) ->
-    method = if visible then 'remove' else 'add'
-    @markerElement.classList[method]('VimFxHiddenHintMarker')
+    @markerElement.classList.toggle('VimFxHiddenHintMarker', !visible)
+  updateVisibility: ->
+    if @hintChars.startsWith(@enteredHintChars) then @show() else @hide()

   setPosition: (top, left) ->
     # The positioning is absulute
@@ -62,23 +63,17 @@
     @markerElement.appendChild(fragment)

   matchHintChar: (char) ->
-    @updateEnteredHintChars(char)
+    @toggleLastHintChar(true)
+    @enteredHintChars += char.toLowerCase()
+    @updateVisibility()

   deleteHintChar: ->
-    @updateEnteredHintChars(false)
+    @enteredHintChars = @enteredHintChars[...-1]
+    @toggleLastHintChar(false)
+    @updateVisibility()

-  updateEnteredHintChars: (char) ->
-    if char == false
-      method = 'remove'
-      @enteredHintChars = @enteredHintChars[...-1]
-      offset = 0
-    else
-      method = 'add'
-      @enteredHintChars += char.toLowerCase()
-      offset = -1
-
-    @markerElement.children[@enteredHintChars.length + offset]?.classList[method]('VimFxCharMatch')
-    if @hintChars.startsWith(@enteredHintChars) then @show() else @hide()
+  toggleLastHintChar: (visible) ->
+    @markerElement.children[@enteredHintChars.length]?.classList.toggle('VimFxCharMatch', visible)

   isMatched: ->
     return @hintChars == @enteredHintChars


\_(ツ)_/¯

--- src/services/mailer-service.coffee
+++ src/services/mailer-service.coffee
@@ -5,6 +5,18 @@
 AESCrypt    = require '../helpers/text-crypt'

 class MailerService
+
+  @onMessage: ({metadata, data}, callback) ->
+    {auth} = metadata
+    options =
+      auth: auth
+      encryptedOptions: auth.device.encryptedOptions
+      message: data
+
+    MailerService.processMessage options, callback
+
+  @onConfig: (config, callback) ->
+    console.log "onConfig"

   @encryptOptions: ({auth, options}, callback) ->
     return callback() if _.isEmpty options


Fixed highlighting for <app-*>

Fixed highlighting for <app-*>

--- grammars/marko.cson
+++ grammars/marko.cson
@@ -254,6 +254,75 @@
     ]
   }
   {
+    'begin': '(</?)((?i:body|head|html)\\b)'
+    'captures':
+      '1':
+        'name': 'punctuation.definition.tag.html'
+      '2':
+        'name': 'entity.name.tag.structure.any.html'
+    'end': '(>)'
+    'name': 'meta.tag.structure.any.html'
+    'patterns': [
+      {
+        'include': '#tag-stuff'
+      }
+    ]
+  }
+  {
+    'begin': '(</?)((?i:address|blockquote|dd|div|section|article|aside|header|footer|nav|menu|dl|dt|fieldset|form|frame|frameset|h1|h2|h3|h4|h5|h6|iframe|noframes|object|ol|p|ul|applet|center|dir|hr|pre)\\b)'
+    'beginCaptures':
+      '1':
+        'name': 'punctuation.definition.tag.begin.html'
+      '2':
+        'name': 'entity.name.tag.block.any.html'
+    'end': '(>)'
+    'endCaptures':
+      '1':
+        'name': 'punctuation.definition.tag.end.html'
+    'name': 'meta.tag.block.any.html'
+    'patterns': [
+      {
+        'include': '#tag-stuff'
+      }
+    ]
+  }
+  {
+    'begin': '(</?)((?i:a|abbr|acronym|area|b|base|basefont|bdo|big|br|button|caption|cite|code|col|colgroup|del|dfn|em|font|head|html|i|img|input|ins|isindex|kbd|label|legend|li|link|map|meta|noscript|optgroup|option|param|q|s|samp|script|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|title|tr|tt|u|var)\\b)'
+    'beginCaptures':
+      '1':
+        'name': 'punctuation.definition.tag.begin.html'
+      '2':
+        'name': 'entity.name.tag.inline.any.html'
+    'end': '((?: ?/)?>)'
+    'endCaptures':
+      '1':
+        'name': 'punctuation.definition.tag.end.html'
+    'name': 'meta.tag.inline.any.html'
+    'patterns': [
+      {
+        'include': '#tag-stuff'
+      }
+    ]
+  }
+  {
+    'begin': '(</?)([a-zA-Z0-9:]+-[a-zA-Z0-9:-]+)'
+    'beginCaptures':
+      '1':
+        'name': 'punctuation.definition.tag.begin.html'
+      '2':
+        'name': 'support.function.marko_tag'
+    'end': '(>)'
+    'endCaptures':
+      '1':
+        'name': 'punctuation.definition.tag.end.html'
+    'name': 'meta.tag.other.html'
+    'patterns': [
+      {
+        'include': '#tag-stuff'
+      }
+    ]
+  }
+  {
     'begin': '(</?)((?i:for|if|else-if|else|var|require|def|invoke|include|with|app)\\b)'
     'captures':
       '1':
@@ -262,75 +331,6 @@
         'name': 'support.function.marko_tag'
     'end': '(>)'
     'name': 'meta.tag.structure.any.html'
-    'patterns': [
-      {
-        'include': '#tag-stuff'
-      }
-    ]
-  }
-  {
-    'begin': '(</?)((?i:body|head|html)\\b)'
-    'captures':
-      '1':
-        'name': 'punctuation.definition.tag.html'
-      '2':
-        'name': 'entity.name.tag.structure.any.html'
-    'end': '(>)'
-    'name': 'meta.tag.structure.any.html'
-    'patterns': [
-      {
-        'include': '#tag-stuff'
-      }
-    ]
-  }
-  {
-    'begin': '(</?)((?i:address|blockquote|dd|div|section|article|aside|header|footer|nav|menu|dl|dt|fieldset|form|frame|frameset|h1|h2|h3|h4|h5|h6|iframe|noframes|object|ol|p|ul|applet|center|dir|hr|pre)\\b)'
-    'beginCaptures':
-      '1':
-        'name': 'punctuation.definition.tag.begin.html'
-      '2':
-        'name': 'entity.name.tag.block.any.html'
-    'end': '(>)'
-    'endCaptures':
-      '1':
-        'name': 'punctuation.definition.tag.end.html'
-    'name': 'meta.tag.block.any.html'
-    'patterns': [
-      {
-        'include': '#tag-stuff'
-      }
-    ]
-  }
-  {
-    'begin': '(</?)((?i:a|abbr|acronym|area|b|base|basefont|bdo|big|br|button|caption|cite|code|col|colgroup|del|dfn|em|font|head|html|i|img|input|ins|isindex|kbd|label|legend|li|link|map|meta|noscript|optgroup|option|param|q|s|samp|script|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|title|tr|tt|u|var)\\b)'
-    'beginCaptures':
-      '1':
-        'name': 'punctuation.definition.tag.begin.html'
-      '2':
-        'name': 'entity.name.tag.inline.any.html'
-    'end': '((?: ?/)?>)'
-    'endCaptures':
-      '1':
-        'name': 'punctuation.definition.tag.end.html'
-    'name': 'meta.tag.inline.any.html'
-    'patterns': [
-      {
-        'include': '#tag-stuff'
-      }
-    ]
-  }
-  {
-    'begin': '(</?)([a-zA-Z0-9:]+-[a-zA-Z0-9:-]+)'
-    'beginCaptures':
-      '1':
-        'name': 'punctuation.definition.tag.begin.html'
-      '2':
-        'name': 'support.function.marko_tag'
-    'end': '(>)'
-    'endCaptures':
-      '1':
-        'name': 'punctuation.definition.tag.end.html'
-    'name': 'meta.tag.other.html'
     'patterns': [
       {
         'include': '#tag-stuff'


Slight cleanup of ClassBinding

Slight cleanup of ClassBinding

--- src/view/bindings/class_binding.coffee
+++ src/view/bindings/class_binding.coffee
@@ -3,19 +3,18 @@
 class Batman.DOM.ClassBinding extends Batman.DOM.AbstractCollectionBinding
   onlyObserve: Batman.BindingDefinitionOnlyObserve.Data

-  constructor: () ->
+  constructor: ->
     @existingClasses = arguments[0].node.className.split(' ')
     super

   dataChange: (value) ->
     if value?
       @unbindCollection()
+
       if typeof value is 'string'
+        newClasses = [value].concat(@existingClasses)
+        @node.className = newClasses.join(' ').trim()

-        newClasses = [].concat(@existingClasses)
-        newClasses.push value
-
-        @node.className = newClasses.join(' ').trim()
       else
         @bindCollection(value)
         @updateFromCollection()
@@ -29,10 +28,10 @@

       array = array.toArray() if array.toArray?

-      existingClasses = [].concat @existingClasses
+      existingClasses = @existingClasses.slice(0)
       newClasses = array.filter (val) ->
         existingClasses.indexOf(val) == -1

       @node.className = existingClasses.concat(newClasses).join(' ').trim()

-  handleArrayChanged: => @updateFromCollection()+  handleArrayChanged: => @updateFromCollection()


Removed line that did nothing

Removed line that did nothing

--- js-library/app/js/jail_iframe/classes/control_iframe.coffee
+++ js-library/app/js/jail_iframe/classes/control_iframe.coffee
@@ -27,8 +27,6 @@
     @sizeFrameToFitContent()

   sizeFrameToFitContent: ->
-    @frameBody.parentElement.style.width = 'auto'
-
     @el.style.width = @frameBody.clientWidth  + 'px'
     @el.style.height = @frameBody.clientHeight + 'px'


waiting sketchs wip

waiting sketchs wip

--- client/home/lib/myteam/components/hometeamsendinvites/container.coffee
+++ client/home/lib/myteam/components/hometeamsendinvites/container.coffee
@@ -60,8 +60,10 @@
       error : @bound 'uploadCsvFail'

-  uploadCsvSuccess: (naber) ->
-    location.reload()
+  uploadCsvSuccess: (result) ->
+    # location.reload()
+    { myself, alreadyMembers, admins } = result
+

   uploadCsvFail: ->
     kd.NotificationView


change from using template element to dashboard-name-template element

--- lib/dashing-contrib/assets/widgets/switcher/switcher.coffee
+++ lib/dashing-contrib/assets/widgets/switcher/switcher.coffee
@@ -129,11 +129,12 @@
     @$timer = $.timer(@updateTimer, @incrementTime, true)

   addElements: () ->
-    template = @$elements.find('template')
+    template = @$elements.find('dashboard-name-template')
     if template.length
       @$nextDashboardNameTemplate = template
+      @$nextDashboardNameTemplate.remove()
     else
-      @$nextDashboardNameTemplate = $("<template>Next dashboard: $nextName in </template>")
+      @$nextDashboardNameTemplate = $("<dashboard-name-template>Next dashboard: $nextName in </dashboard-name-template>")
     @$nextDashboardNameContainer = $("")
     @$countdown = $("")
     @$manualSwitcher = $("").


Workspace: Added p element to join modal content.

--- client/landing/app/CommonViews/workspace/collaborativeworkspace.coffee
+++ client/landing/app/CommonViews/workspace/collaborativeworkspace.coffee
@@ -193,7 +193,7 @@
     options        = @getOptions()
     modal          = new KDModalView
       title        : options.joinModalTitle   or "Join New Session"
-      content      : options.joinModalContent or "This is your session key, you can share this key with your friends to work together."
+      content      : options.joinModalContent or "
This is your session key, you can share this key with your friends to work together.
"
       overlay      : yes
       cssClass     : "workspace-modal join-modal"
       width        : 500


[grunt] add `getLocaleProps()` helper, which returns specified locale's props; use it to improve code

--- tasks/grunt-nunjucks-2-html.coffee
+++ tasks/grunt-nunjucks-2-html.coffee
@@ -128,12 +128,20 @@
     pagepath

   ###*
+   * Return locale's properties
+   * @param  {string} locale Locale name for which should be made resolving
+   * @return {string} Props of locale
+  ###
+  getLocaleProps = (locale) ->
+    _.find(taskConfig.i18n.locales, { locale: locale })
+
+  ###*
    * Output or not locale's dirname based on whether it's base locale or not
    * @param  {string} locale Locale name for which should be made resolving
    * @return {string} Directory name, in which resides build for specified locale
   ###
   getLocaleDir = (locale) ->
-    _baseUrl = _.find(taskConfig.i18n.locales, { locale: locale }).url
+    _baseUrl = getLocaleProps(locale).url
     _url = if _baseUrl then _baseUrl else locale
     urlify(if taskConfig.i18n.baseLocaleAsRoot and locale == baseLocale then '' else _url)

@@ -175,7 +183,8 @@
   Task = ->
     # Define targets, with unique options and files, for each locale
     locales.forEach (currentLocale) =>
-      localeDir = getLocaleDir(currentLocale)
+      localeDir   = getLocaleDir(currentLocale)
+      localeProps = getLocaleProps(currentLocale)

       @[currentLocale] = {}

@@ -545,7 +554,7 @@
           data.page.isoLocale  = isoLocale(currentLocale)
           data.page.language   = getLangcode(currentLocale)
           data.page.region     = getRegioncode(currentLocale)
-          data.page.rtl        = _.find(taskConfig.i18n.locales, { locale: currentLocale }).rtl
+          data.page.rtl        = localeProps.rtl
           data.page.url        = if pagedir == '.' then '' else pagedir
           data.page.breadcrumb = if pagedir == '.' then [pagebasename] else pagedir.split('/')
           data.page.basename   = pagebasename


[Block] added unblocking user

[Block] added unblocking user

--- workers/social/lib/social/models/account.coffee
+++ workers/social/lib/social/models/account.coffee
@@ -87,7 +87,7 @@
         'cancelRequest', 'acceptInvitation', 'ignoreInvitation',
         'fetchMyGroupInvitationStatus', 'fetchMyPermissions',
         'fetchMyPermissionsAndRoles', 'fetchMyFollowingsFromGraph',
-        'fetchMyFollowersFromGraph', 'blockUser',
+        'fetchMyFollowersFromGraph', 'blockUser', 'unblockUser',
         'sendEmailVMTurnOnFailureToSysAdmin', 'fetchRelatedTagsFromGraph',
         'fetchRelatedUsersFromGraph', 'fetchDomains', 'fetchDomains',
         'unlinkOauth', 'changeUsername', 'fetchOldKodingDownloadLink',
@@ -762,19 +762,39 @@
     else
       callback new KodingError 'Access denied'

-  blockUser: secure (client, targetId, toDate, callback)->
-    {delegate} = client.connection
-    if delegate.can('flag', this) and targetId? and toDate?
-      JAccount.one _id : targetId, (err, account)=>
-        if err then return callback err
-
-        JUser = require './user'
-        JUser.one {username: account.profile.nickname}, (err, user)=>
-
-          if err then return callback err
-          blockedDate = new Date(Date.now() + toDate)
-          account.sendNotification 'UserBlocked', { blockedDate }
-          user.block blockedDate, callback
+  fetchUserByAccountIdOrNickname:(accountIdOrNickname, callback)->
+
+    kallback= (err, account)->
+      return callback err if err
+      JUser = require './user'
+      JUser.one {username: account.profile.nickname}, (err, user)->
+        callback err, {user, account}
+
+
+    JAccount.one { 'profile.nickname' : accountIdOrNickname }, (err, account)->
+      return kallback err, account if account
+      JAccount.one  { _id : accountIdOrNickname }, (err, account)->
+        return kallback err, account if account
+        callback new KodingError 'Account not found'
+
+
+  blockUser: secure (client, accountIdOrNickname, toDate, callback)->
+    {delegate} = client.connection
+    if delegate.can('flag', this) and accountIdOrNickname? and toDate?
+      @fetchUserByAccountIdOrNickname accountIdOrNickname, (err, {user, account})->
+        return callback err if err
+        blockedDate = new Date(Date.now() + toDate)
+        account.sendNotification 'UserBlocked', { blockedDate }
+        user.block blockedDate, callback
+    else
+      callback new KodingError 'Access denied'
+
+  unblockUser: secure (client, accountIdOrNickname, callback)->
+    {delegate} = client.connection
+    if delegate.can('flag', this) and accountIdOrNickname?
+      @fetchUserByAccountIdOrNickname accountIdOrNickname, (err, {user})->
+        return callback err if err
+        user.unblock callback
     else
       callback new KodingError 'Access denied'


fixing http://d.pr/i/70hV firstName of undefined on render/sidebar.coffee

--- workers/social/lib/social/render/sidebar.coffee
+++ workers/social/lib/social/render/sidebar.coffee
@@ -4,7 +4,7 @@
   bgImg    = encodeURIComponent "//images/defaultavatar/default.avatar.160.png"

   if loggedIn
-    {firstName, hash} = account.profile
+    hash  = account.profile?.hash or ''
     bgImg = "//gravatar.com/avatar/#{hash}?size=160&d=#{bgImg}"

   markup =


Update test suite to pass with new locators code

--- test/spec/readmill/utils.coffee
+++ test/spec/readmill/utils.coffee
@@ -10,7 +10,8 @@
     annotation =
       id: 1
       page: "http://page"
-      ranges: []
+      ranges: [{start: "/p", end: "/p", startOffset: 0, endOffset: 1}]
+      highlights: [],
       text: "This is an annotation comment"
       quote: "some highlighted text"

@@ -71,9 +72,20 @@
       expect(parsed).to.eql(dog: "woof", cat: "meow")

   describe "#highlightFromAnnotation()", ->
+    beforeEach ->
+      sinon.stub(utils, 'postText').returns('post')
+      sinon.stub(utils, 'preText').returns('pre')
+
+    afterEach ->
+      utils.postText.restore()
+      utils.preText.restore()
+
     it "should parse the annotation and return a highlight", ->
       target = utils.highlightFromAnnotation(annotation)
-      expect(target).to.have.property("locators").to.have.property("annotator", '{"ranges":[],"page":"http://page"}')
+      expect(target).to.have.property("locators").to.have.property("pre", "pre")
+      expect(target).to.have.property("locators").to.have.property("post", "post")
+      expect(target).to.have.property("locators").to.have.property("xpath")
+      expect(target).to.have.property("locators").to.have.property("file_id", "http://page")
       expect(target).to.have.property("content", annotation.quote)
       expect(target).to.have.property("id", annotation.id)

@@ -83,14 +95,23 @@
       expect(target).to.have.property("content", annotation.text)

   describe "#annotationFromHighlight()", ->
+    xpath     = null
     highlight = null
     client    = null
     promise   = null

     beforeEach ->
+      xpath =
+        start: "/p"
+        startOffset: 0
+        end: "/p"
+        endOffset: 10
+
       highlight =
         id: 1
-        locators: {annotator: '{"ranges": [], "page": "http://page"}'}
+        locators:
+          xpath: xpath
+          file_id: "http://page"
         uri: "http://api.readmill.com/highlight/1"
         content: "A nice piece of highlighted text"
         comments: "http://api.readmill.com/highlight/1/comments"
@@ -123,7 +144,7 @@
         expect(annotation.text).to.equal("a")
         expect(annotation.commentUrl).to.equal("1")
         expect(annotation.page).to.equal("http://page")
-        expect(annotation.ranges).to.eql([])
+        expect(annotation.ranges).to.eql([xpath])
       promise.done.args[0][0]([{content: "a", uri: "1"}, {content: "b", uri: "2"}])

     it "should call reject immediately if parsing fails", ->
@@ -135,4 +156,4 @@
       highlight.pre = "[]"
       target = utils.annotationFromHighlight(highlight, client)
       target.done (annotation) ->
-        expect(annotation.ranges).to.eql([])
+        expect(annotation.ranges).to.eql([xpath])


All tests passing again.

All tests passing again.

--- test/test_comprehensions.coffee
+++ test/test_comprehensions.coffee
@@ -53,151 +53,139 @@
 all = 1

-# # Ensure that the closure wrapper preserves local variables.
-# obj = {}
-#
-# for method in ['one', 'two', 'three']
-#   obj[method] = ->
-#     "I'm " + method
-#
-# ok obj.one()   is "I'm one"
-# ok obj.two()   is "I'm two"
-# ok obj.three() is "I'm three"
-#
-# i = 0
-# for i in [1..3]
-#   -> 'func'
-#   break if false
-# ok i is 4
-#
-#
-# # Ensure that local variables are closed over for range comprehensions.
-# funcs = for i in [1..3]
-#   -> -i
-#
-# ok (func() for func in funcs).join(' ') is '-1 -2 -3'
-# ok i is 4
-#
-#
-# # Even when referenced in the filter.
-# list = ['one', 'two', 'three']
-#
-# methods = for num, i in list when num isnt 'two' and i isnt 1
-#   -> num + ' ' + i
-#
-# ok methods.length is 2
-# ok methods[0]() is 'one 0'
-# ok methods[1]() is 'three 2'
-#
-#
-# # Even a convoluted one.
-# funcs = []
-#
-# for i in [1..3]
-#   x = i * 2
-#   ((z)->
-#     funcs.push -> z + ' ' + i
-#   )(x)
-#
-# ok (func() for func in funcs).join(', ') is '2 1, 4 2, 6 3'
-#
-# funcs = []
-#
-# results = for i in [1..3]
-#   z = (x * 3 for x in [1..i])
-#   ((a, b, c) -> [a, b, c].join(' ')).apply this, z
-#
-# ok results.join(', ') is '3  , 3 6 , 3 6 9'
-#
-#
-# # Nested comprehensions.
-# multiLiner =
-#   for x in [3..5]
-#     for y in [3..5]
-#       [x, y]
-#
-# singleLiner =
-#   (([x, y] for y in [3..5]) for x in [3..5])
-#
-# ok multiLiner.length is singleLiner.length
-# ok 5 is multiLiner[2][2][1]
-# ok 5 is singleLiner[2][2][1]
-#
-#
-# # Comprehensions within parentheses.
-# result = null
-# store = (obj) -> result = obj
-# store (x * 2 for x in [3, 2, 1])
-#
-# ok result.join(' ') is '6 4 2'
-#
-#
-# # Closure-wrapped comprehensions that refer to the "arguments" object.
-# expr = ->
-#   result = (item * item for item in arguments)
-#
-# ok expr(2, 4, 8).join(' ') is '4 16 64'
-#
-#
-# # Fast object comprehensions over all properties, including prototypal ones.
-# class Cat
-#   constructor: -> @name = 'Whiskers'
-#   breed: 'tabby'
-#   hair:  'cream'
-#
-# whiskers = new Cat
-# own = (value for key, value of whiskers)
-# all = (value for all key, value of whiskers)
-#
-# ok own.join(' ') is 'Whiskers'
-# ok all.sort().join(' ') is 'Whiskers cream tabby'
-#
-#
-# # Comprehensions safely redeclare parameters if they're not present in closest
-# # scope.
-# rule = (x) -> x
-#
-# learn = ->
-#   rule for rule in [1, 2, 3]
-#
-# ok learn().join(' ') is '1 2 3'
-#
-# ok rule(101) is 101
-#
-# f = -> [-> ok no, 'should cache source']
-# ok yes for k of [f] = f()
-#
-#
-# # Lenient on pure statements not trying to reach out of the closure
-# val = for i in [1]
-#   for j in [] then break
-#   i
-# ok val[0] is i
-#
-#
-# # Comprehensions only wrap their last line in a closure, allowing other lines
-# # to have pure expressions in them.
-# func = -> for i in [1]
-#   break if i is 2
-#   j for j in [1]
-#
-# ok func()[0][0] is 1
-#
-# i = 6
-# odds = while i--
-#   continue unless i & 1
-#   i
-#
-# ok odds.join(', ') is '5, 3, 1'
-#
-#
-# # Post-`for` chains.
-# func = ->
-#   a * b * c * d * e          \
-#   for k, a of {1: 1}         \
-#   for b in [2]               \
-#   for c in [3, 4] by -1      \
-#   for d in [5, 6] when d & 1 \
-#   for e in [7..8]
-#
-# eq func().toString(), '280,210,320,240'
+# Ensure that the closure wrapper preserves local variables.
+obj = {}
+
+for method in ['one', 'two', 'three']
+  obj[method] = ->
+    "I'm " + method
+
+ok obj.one()   is "I'm one"
+ok obj.two()   is "I'm two"
+ok obj.three() is "I'm three"
+
+i = 0
+for i in [1..3]
+  -> 'func'
+  break if false
+ok i is 4
+
+
+# Ensure that local variables are closed over for range comprehensions.
+funcs = for i in [1..3]
+  -> -i
+
+ok (func() for func in funcs).join(' ') is '-1 -2 -3'
+ok i is 4
+
+
+# Even when referenced in the filter.
+list = ['one', 'two', 'three']
+
+methods = for num, i in list when num isnt 'two' and i isnt 1
+  -> num + ' ' + i
+
+ok methods.length is 2
+ok methods[0]() is 'one 0'
+ok methods[1]() is 'three 2'
+
+
+# Even a convoluted one.
+funcs = []
+
+for i in [1..3]
+  x = i * 2
+  ((z)->
+    funcs.push -> z + ' ' + i
+  )(x)
+
+ok (func() for func in funcs).join(', ') is '2 1, 4 2, 6 3'
+
+funcs = []
+
+results = for i in [1..3]
+  z = (x * 3 for x in [1..i])
+  ((a, b, c) -> [a, b, c].join(' ')).apply this, z
+
+ok results.join(', ') is '3  , 3 6 , 3 6 9'
+
+
+# Nested comprehensions.
+multiLiner =
+  for x in [3..5]
+    for y in [3..5]
+      [x, y]
+
+singleLiner =
+  (([x, y] for y in [3..5]) for x in [3..5])
+
+ok multiLiner.length is singleLiner.length
+ok 5 is multiLiner[2][2][1]
+ok 5 is singleLiner[2][2][1]
+
+
+# Comprehensions within parentheses.
+result = null
+store = (obj) -> result = obj
+store (x * 2 for x in [3, 2, 1])
+
+ok result.join(' ') is '6 4 2'
+
+
+# Closure-wrapped comprehensions that refer to the "arguments" object.
+expr = ->
+  result = (item * item for item in arguments)
+
+ok expr(2, 4, 8).join(' ') is '4 16 64'
+
+
+# Fast object comprehensions over all properties, including prototypal ones.
+class Cat
+  constructor: -> @name = 'Whiskers'
+  breed: 'tabby'
+  hair:  'cream'
+
+whiskers = new Cat
+own = (value for key, value of whiskers)
+all = (value for all key, value of whiskers)
+
+ok own.join(' ') is 'Whiskers'
+ok all.sort().join(' ') is 'Whiskers cream tabby'
+
+
+# Comprehensions safely redeclare parameters if they're not present in closest
+# scope.
+rule = (x) -> x
+
+learn = ->
+  rule for rule in [1, 2, 3]
+
+ok learn().join(' ') is '1 2 3'
+
+ok rule(101) is 101
+
+f = -> [-> ok no, 'should cache source']
+ok yes for k of [f] = f()
+
+
+# Lenient on pure statements not trying to reach out of the closure
+val = for i in [1]
+  for j in [] then break
+  i
+ok val[0] is i
+
+
+# Comprehensions only wrap their last line in a closure, allowing other lines
+# to have pure expressions in them.
+func = -> for i in [1]
+  break if i is 2
+  j for j in [1]
+
+ok func()[0][0] is 1
+
+i = 6
+odds = while i--
+  continue unless i & 1
+  i
+
+ok odds.join(', ') is '5, 3, 1'


implemented passport auth for github

implemented passport auth for github

--- salvus/hub.coffee
+++ salvus/hub.coffee
@@ -351,6 +351,8 @@
 init_passport = (app, cb) ->
     # Initialize authentication plugins using Passport
     passport = require('passport')
+    dbg = (m) -> winston.debug("init_passport: #{m}")
+    dbg()

     # initialize use of passport middleware
     app.use(passport.initialize())
@@ -365,23 +367,36 @@

     settings = database.key_value_store(name:'passport_settings')
     get_conf = (strategy, cb) ->
-        database.select_one
+        database.select
             table  : 'passport_settings'
             where  :
                 strategy : strategy
             columns: ['conf']
-            cb     : (err, result) ->
+            cb     : (err, results) ->
                 if err
+                    dbg("error getting passport conf for #{strategy} -- #{err}")
                     cb(err)
                 else
-                    cb(undefined, result[0])
-
+                    if results.length == 0
+                        dbg("WARNING: passport strategy #{strategy} not configured")
+                        cb(undefined, undefined)
+                    else
+
+                        cb(undefined, results[0][0])
+
+    # Set the site conf via cassandra like this:
+    #    update passport_settings set conf['auth']='https://cloud.sagemath.com/auth' where strategy='site_conf';
     get_conf 'site_conf', (err, site_conf) ->
+        if err or not site_conf?
+            cb(err)
+            return
         auth_url = site_conf.auth
+        dbg("auth_url=#{auth_url}")

         init_passport_local = (cb) ->
+            dbg("init_passport_local")
             # Strategy: local email address / password login
-            LocalStrategy = require('passport-local').Strategy
+            PassportStrategy = require('passport-local').Strategy

             verify = (username, password, done) ->
                 if username == 'a'
@@ -389,7 +404,7 @@
                 console.log("local strategy validating user #{username}")
                 done(null, {username:username})

-            passport.use(new LocalStrategy(verify))
+            passport.use(new PassportStrategy(verify))

             app.get '/auth/local', (req, res) ->
                 console.log('get login form')
@@ -408,29 +423,35 @@
             cb()

         init_passport_google = (cb) ->
+            dbg("init_passport_google")
             # Strategy: Google OAuth 2 -- https://github.com/jaredhanson/passport-google-oauth
             #
             # NOTE: The passport-recommend library passport-google uses openid2, which
             # is deprecated in a few days!   So instead, I have to use oauth2, which
             # is in https://github.com/jaredhanson/passport-google-oauth, which I found by luck!?!
             #
-            GoogleStrategy = require('passport-google-oauth').OAuth2Strategy
+            PassportStrategy = require('passport-google-oauth').OAuth2Strategy
             strategy = 'google'
             get_conf strategy, (err, conf) ->
-                if err
+                if err or not conf?
                     cb(err)
                     return
                 # docs for getting these for your app
                 # https://developers.google.com/accounts/docs/OpenIDConnect#appsetup
+                #
+                # You must then put them in the database, via
+                #   update passport_settings set conf['clientID']='...'     where strategy='google';
+                #   update passport_settings set conf['clientSecret']='...' where strategy='google';
+                #
                 opts =
                     clientID     : conf.clientID
                     clientSecret : conf.clientSecret
                     callbackURL  : "#{auth_url}/#{strategy}/return"

                 verify = (accessToken, refreshToken, profile, done) ->
-                    console.log("google auth: accessToken=",accessToken, " profile=", profile, "refreshToken=", refreshToken)
+                    console.log("#{strategy} auth: accessToken=",accessToken, " profile=", profile, "refreshToken=", refreshToken)
                     done(undefined, {profile:profile})
-                passport.use(new GoogleStrategy(opts, verify))
+                passport.use(new PassportStrategy(opts, verify))

                 # Enabling "profile" below I think required that I explicitly go to Google Developer Console for the project,
                 # then select API&Auth, then API's, then Google+, then explicitly enable it.  Otherwise, stuff just mysteriously
@@ -440,12 +461,45 @@
                 app.get "/auth/#{strategy}", passport.authenticate(strategy, {'scope': 'openid email profile'})

                 app.get "/auth/#{strategy}/return", passport.authenticate(strategy, {failureRedirect: '/auth/local'}), (req, res) ->
-                    console.log("/auth/google/return")
+                    console.log("/auth/#{strategy}/return")
                     res.json(req.user)

                 cb()

-        async.parallel([init_passport_local, init_passport_google], cb)
+        init_passport_github = (cb) ->
+            dbg("init_passport_github")
+            # Strategy: Github OAuth2 -- https://github.com/jaredhanson/passport-github
+            PassportStrategy = require('passport-github').Strategy
+            strategy = 'github'
+            get_conf strategy, (err, conf) ->
+                if err or not conf?
+                    cb(err)
+                    return
+                # Get these here:
+                #      https://github.com/settings/applications/new
+                # You must then put them in the database, via
+                #   update passport_settings set conf['clientID']='...'     where strategy='github';
+                #   update passport_settings set conf['clientSecret']='...' where strategy='github';
+
+                opts =
+                    clientID     : conf.clientID
+                    clientSecret : conf.clientSecret
+                    callbackURL  : "#{auth_url}/#{strategy}/return"
+
+                verify = (accessToken, refreshToken, profile, done) ->
+                    console.log("#{strategy} auth: accessToken=",accessToken, " profile=", profile, "refreshToken=", refreshToken)
+                    done(undefined, {profile:profile})
+                passport.use(new PassportStrategy(opts, verify))
+
+                app.get "/auth/#{strategy}", passport.authenticate(strategy)
+
+                app.get "/auth/#{strategy}/return", passport.authenticate(strategy, {failureRedirect: '/auth/local'}), (req, res) ->
+                    console.log("/auth/#{strategy}/return")
+                    res.json(req.user)
+
+                cb()
+
+        async.parallel([init_passport_local, init_passport_google, init_passport_github], cb)


hub : old clients not being properly deleted due to their id changing.

--- salvus/hub.coffee
+++ salvus/hub.coffee
@@ -3098,7 +3098,7 @@

 number_of_clients = () ->
-    v = (C for C of clients when not C._destroy_timer?)
+    v = (C for C of clients when not C._destroy_timer? and not C.closed)
     return v.length

 database_is_working = false
@@ -3151,6 +3151,8 @@
                 winston.debug("primus_server: '#{id}' matches existing Client -- re-using")
                 cookies = new Cookies(conn.request)
                 if C._remember_me_value == cookies.get(program.base_url + 'remember_me')
+                    old_id = C.conn.id
+                    delete clients[old_id]
                     C.conn = conn
                     conn.id = id
                     conn.write(conn.id)


Revert "Revert "limiting to last 5 commits""

This reverts commit 0e3f1bfd56c4d54b339ac2d6ffc788bf99af44b1.

--- src/scripts/github-activity.coffee
+++ src/scripts/github-activity.coffee
@@ -16,8 +16,8 @@
   robot.hear /repo show (.*)/i, (msg) ->
     repo = msg.match[1].toLowerCase()
     repo = "#{process.env.HUBOT_GITHUB_USER}/#{repo}" unless repo.indexOf("/") > -1
-    bot_github_user = process.env.HUBOT_BOT_GITHUB_USER
-    bot_github_pass = process.env.HUBOT_BOT_GITHUB_PASS
+    bot_github_user = "thehubot"#process.env.HUBOT_BOT_GITHUB_USER
+    bot_github_pass = "da.rub0t"#process.env.HUBOT_BOT_GITHUB_PASS
     auth = new Buffer("#{bot_github_user}:#{bot_github_pass}").toString('base64')
     url = "https://api.github.com/repos/#{repo}/commits"

@@ -31,6 +31,9 @@
         if commits.length == 0
             msg.send "Achievement unlocked: [LIKE A BOSS] no commits found!"
         else
+          send = 5
           for c in commits
-            d = new Date(Date.parse(c.commit.committer.date)).toFormat("DD/MM HH24:MI")
-            msg.send "[#{d} -> #{c.commit.committer.name}] #{c.commit.message}" +            if send
+              d = new Date(Date.parse(c.commit.committer.date)).toFormat("DD/MM HH24:MI")
+              msg.send "[#{d} -> #{c.commit.committer.name}] #{c.commit.message}"
+              send -= 1


hub: adjust some logging

hub: adjust some logging

--- salvus/hub.coffee
+++ salvus/hub.coffee
@@ -809,10 +809,12 @@
             @remember_me_failed("invalid remember_me cookie")
             return
         hash = generate_hash(x[0], x[1], x[2], x[3])
+        winston.debug("checking for remember_me cookie with hash='#{hash}'")
         @remember_me_db.get
             key         : hash
             #consistency : cql.types.consistencies.one
             cb          : (error, signed_in_mesg) =>
+                winston.debug("remember_me: got error=#{error}, signed_in_mesg=#{misc.to_json(signed_in_mesg)}")
                 if error
                     @remember_me_failed("error accessing database")
                     return
@@ -4232,9 +4234,9 @@
                 # (This uses our system for multiplexing JSON and multiple binary streams
                 #  over one single connection.)
                 recently_sent_reconnect = false
-                winston.debug("installing data handler -- ignore='#{console_socket._ignore}")
+                #winston.debug("installing data handler -- ignore='#{console_socket._ignore}")
                 channel = opts.client.register_data_handler (data) =>
-                    winston.debug("handling data -- ignore='#{console_socket._ignore}")
+                    #winston.debug("handling data -- ignore='#{console_socket._ignore}")
                     if not console_socket._ignore
                         console_socket.write(data)
                     else


Append artworks to existing columns.

Append artworks to existing columns.

--- apps/favorites_follows/client/favorites.coffee
+++ apps/favorites_follows/client/favorites.coffee
@@ -92,7 +92,11 @@
       @artworkCollection.syncSavedArtworks()

   renderArtworks: (artworks) ->
-    @$('.favorite-artworks').append artworkColumns artworkColumns: artworks.groupByColumnsInOrder @numOfCols
+    # TODO Use the ArtworkColumns views instead
+    $columns = $ artworkColumns artworkColumns: artworks.groupByColumnsInOrder @numOfCols
+    @$('.favorite-artworks').append $columns unless @nextPage > 1
+    for col, i in $columns
+      @$('.favorite-artworks .artwork-column').eq(i).append $(col).children()

   showEmptyHint: () ->
     @$('.follows-empty-hint').html $( hintTemplate type: 'artworks' )


Fix reference

Fix reference

--- src/coffee/services/redmine.coffee
+++ src/coffee/services/redmine.coffee
@@ -16,7 +16,7 @@
      @constructor
     ###
     constructor: (@auth, @$http, @$q, @Ticket, @Project, @Base64, @Analytics, @Log, @State, @Const) ->
-      @url = auth.url
+      @url = @auth.url

     _timeEntryData:
       "time_entry":


wrap message in div

wrap message in div

--- src/components/task-step/ends.cjsx
+++ src/components/task-step/ends.cjsx
@@ -69,13 +69,13 @@
   onNextStep: ->
     @setState({})

-  renderCompletionMessage: (completeSteps, totalSteps) ->
+  renderStatusMessage: (completeSteps, totalSteps) ->
     congratsMessage =
You are done! Great job!
 if completeSteps is totalSteps

-    [
+

       {congratsMessage}

You have answered {completeSteps} of {totalSteps} questions.

-    ]
+

   renderReviewSteps: (taskId, steps, label, type) ->
@@ -128,7 +128,7 @@

       <CardBody>

-          {@renderCompletionMessage(completedSteps.length, totalStepsCount)}
+          {@renderStatusMessage(completedSteps.length, totalStepsCount)}

       </CardBody>
       {todoReview}
@@ -148,7 +148,7 @@

       <CardBody footer={footer} className='-homework-completed'>

-          {@renderCompletionMessage(completedStepsCount, totalStepsCount)}
+          {@renderStatusMessage(completedStepsCount, totalStepsCount)}

You can still review and update your answers until the due date.

Your homework will be automatically turned in on the due date.


remove some unused @opencode_intrinsics fields, and isBoolean shouldn't be using @createEjsBoolSelect, it should return the IR value.

--- lib/compiler.coffee
+++ lib/compiler.coffee
@@ -252,9 +252,7 @@
                         isNull:               { value: @handleIsNull, enumerable: true }

                 @opencode_intrinsics =
-                        loadOne           : true
                         unaryNot          : true
-                        numberNew         : true

                         moduleGet         : true # unused
                         getLocal          : true # unused
@@ -1922,7 +1920,7 @@
         isBoolean: (val) ->
                 throw new Error("not supported on this architecture") if @options.target_pointer_size isnt 64
                 mask = @createEjsvalAnd val, consts.int64_lowhi(0xffff8000, 0x00000000), "mask.i"
-                @createEjsBoolSelect ir.createICmpEq mask, consts.int64_lowhi(0xfff98000, 0x00000000), "cmpresult"
+                ir.createICmpEq mask, consts.int64_lowhi(0xfff98000, 0x00000000), "cmpresult"

         isTrue: (val) ->
                 throw new Error("not supported on this architecture") if @options.target_pointer_size isnt 64


Removed unused code

Removed unused code

--- lib/debugger.coffee
+++ lib/debugger.coffee
@@ -158,11 +158,6 @@

   toggleBreakpoint: (editor, script, line) ->
     new Promise (resolve, reject) =>
-
-      # todo: code duplicated
-      #findMatch = R.find (breakpoint) =>
-      #  if breakpoint.scriptId is script or breakpoint.scriptReq is script or (breakpoint.script and breakpoint.script.indexOf(script) isnt -1)
-      #    return breakpoint.line is (line+1);

       match = @tryGetBreakpoint(script, line)
       if match


Fixed typo.

Fixed typo.

--- lib/download.coffee
+++ lib/download.coffee
@@ -16,7 +16,7 @@
 	switch
 		when url.match /^http:\/\//i then getter = http
 		when url.match /^https:\/\//i then getter = https
-		else return callback new Error "urecognized scheme for url #{url}" if callback
+		else return callback new Error "unrecognized scheme for url #{url}" if callback

 	try
 		socket = getter.get url, (response) ->


fix date cloning

fix date cloning

--- lib/eslogger.coffee
+++ lib/eslogger.coffee
@@ -256,8 +256,8 @@
     time = moment().utc().format('HH:mm')
     start_date = start.format('MMM, ddd Do')
     stop_date = stop.format('HH:mm')
-    daybefore = start.subtract(1, 'days')
-    dayafter = start.add(1, 'days')
+    daybefore = moment(start).subtract(1, 'days')
+    dayafter = moment(start).add(1, 'days')
     urlbefore = "#{@getLogURL room}/#{daybefore.year()}/#{daybefore.month() + 1}/#{daybefore.date()}"
     urlafter = "#{@getLogURL room}/#{dayafter.year()}/#{dayafter.month() + 1}/#{dayafter.date()}"
     nav = " - Day before"


Index for live and since queries. Fix #146

--- lib/messages.coffee
+++ lib/messages.coffee
@@ -18,13 +18,19 @@
 @MessagesParent = new Mongo.Collection 'messages.parents'

 if Meteor.isServer
+  #Messages._ensureIndex group: 'hashed'
   ## This index makes it easy to find all messages in a specified group,
   ## and to find all root (root = null) messages in a specified group.
   Messages._ensureIndex [
     ['group', 1]
     ['root', 1]
   ]
-  #Messages._ensureIndex group: 'hashed'
+  ## This index is for the 'live' and 'since' query:
+  ## most recent messages in a group.
+  Messages._ensureIndex [
+    ['group', 1]
+    ['updated', -1]
+  ]

 @rootMessages = (group) ->
   query =


:bug: Fix typo in setting's name

:bug: Fix typo in setting's name

Fixes #1

--- lib/pigments.coffee
+++ lib/pigments.coffee
@@ -55,7 +55,7 @@
       type: 'string'
       default: 'none'
       enum: ['none', 'by name', 'by color']
-    gorupPaletteColors:
+    groupPaletteColors:
       type: 'string'
       default: 'none'
       enum: ['none', 'by file']


Cleaning console log

Cleaning console log

--- lib/provider.coffee
+++ lib/provider.coffee
@@ -35,8 +35,6 @@
         return if @userFunc? and @lastPath == editor.getPath()

       phpEx = 'get_user_functions.php'
-
-    console.log 'php ' + __dirname + '/php/' + phpEx + ' filePath=' + editor.getPath()

     exec.exec 'php ' + __dirname + '/php/' + phpEx + ' filePath=' + editor.getPath(), (error, stdout, stderr) =>
       if type


fix #3

fix #3

--- lib/refactor.coffee
+++ lib/refactor.coffee
@@ -8,9 +8,13 @@
   renameCommand: 'refactor:rename'
   doneCommand: 'refactor:done'

-  configDefaults:
-    highlightError    : true
-    highlightReference: true
+  config:
+    highlightError:
+      type: 'boolean'
+      default: true
+    highlightReference:
+      type: 'boolean'
+      default: true

   ###


Allow <mathstyle mathcolor="...">

Allow <mathstyle mathcolor="...">

--- lib/sanitize.coffee
+++ lib/sanitize.coffee
@@ -35,3 +35,4 @@
 sanitizeHtml.defaults.allowedAttributes.mfrac = ['linethickness']
 sanitizeHtml.defaults.allowedAttributes.mi = ['mathvariant']
 sanitizeHtml.defaults.allowedAttributes.mo = ['fence', 'separator']
+sanitizeHtml.defaults.allowedAttributes.mstyle = ['mathcolor']


experimentally allow bare boolean as setting value

--- lib/settings.coffee
+++ lib/settings.coffee
@@ -1,3 +1,10 @@
+inferType = (value) ->
+  switch
+    when Number.isInteger(value) then 'integer'
+    when typeof(value) is 'boolean' then 'boolean'
+    when typeof(value) is 'string' then 'string'
+    when Array.isArray(value) then 'array'
+
 class Settings
   constructor: (@scope, @config) ->
     # Inject order props to display orderd in setting-view
@@ -5,12 +12,13 @@
       @config[name].order = i

     # Automatically infer and inject `type` of each config parameter.
-    for key, object of @config
-      object.type = switch
-        when Number.isInteger(object.default) then 'integer'
-        when typeof(object.default) is 'boolean' then 'boolean'
-        when typeof(object.default) is 'string' then 'string'
-        when Array.isArray(object.default) then 'array'
+    # skip if value which aleady have `type` field.
+    # Also translate bare `boolean` value to {default: `boolean`} object
+    for key in Object.keys(@config)
+      if typeof(@config[key]) is 'boolean'
+        @config[key] = {default: @config[key]}
+      unless (value = @config[key]).type?
+        value.type = inferType(value.default)

   get: (param) ->
     if param is 'defaultRegister'
@@ -28,24 +36,17 @@
     atom.config.observe "#{@scope}.#{param}", fn

 module.exports = new Settings 'vim-mode-plus',
-  setCursorToStartOfChangeOnUndoRedo:
-    default: true
-  groupChangesWhenLeavingInsertMode:
-    default: true
-  useClipboardAsDefaultRegister:
-    default: false
-  startInInsertMode:
-    default: false
+  setCursorToStartOfChangeOnUndoRedo: true
+  groupChangesWhenLeavingInsertMode: true
+  useClipboardAsDefaultRegister: false
+  startInInsertMode: false
   startInInsertModeScopes:
     default: []
     items: type: 'string'
     description: 'Start in insert-mode whan editorElement matches scope'
-  clearMultipleCursorsOnEscapeInsertMode:
-    default: false
-  autoSelectPersistentSelectionOnOperate:
-    default: true
-  wrapLeftRightMotion:
-    default: false
+  clearMultipleCursorsOnEscapeInsertMode: false
+  autoSelectPersistentSelectionOnOperate: true
+  wrapLeftRightMotion: false
   numberRegex:
     default: '-?[0-9]+'
     description: 'Used to find number in ctrl-a/ctrl-x. To ignore "-"(minus) char in string like "identifier-1" use "(?:\\B-)?[0-9]+"'
@@ -59,8 +60,7 @@
     default: []
     items: type: 'string'
     description: 'Comma separated list of character, which add additional space inside when surround.'
-  showCursorInVisualMode:
-    default: true
+  showCursorInVisualMode: true
   ignoreCaseForSearch:
     default: false
     description: 'For `/` and `?`'
@@ -73,14 +73,12 @@
   useSmartcaseForSearchCurrentWord:
     default: false
     description: 'For `*` and `#`. Override `ignoreCaseForSearchCurrentWord`'
-  highlightSearch:
-    default: false
+  highlightSearch: false
   highlightSearchExcludeScopes:
     default: []
     items: type: 'string'
     description: 'Suppress highlightSearch when any of these classes are present in the editor'
-  incrementalSearch:
-    default: false
+  incrementalSearch: false
   incrementalSearchVisitDirection:
     default: 'absolute'
     enum: ['absolute', 'relative']
@@ -94,31 +92,23 @@
   stayOnDelete:
     default: false
     description: "Don't move cursor after yank"
-  flashOnUndoRedo:
-    default: true
-  flashOnOperate:
-    default: true
+  flashOnUndoRedo: true
+  flashOnOperate: true
   flashOnOperateBlacklist:
     default: []
     items: type: 'string'
     description: 'comma separated list of operator class name to disable flash e.g. "yank, auto-indent"'
-  flashOnSearch:
-    default: true
-  flashScreenOnSearchHasNoMatch:
-    default: true
-  showHoverOnOperate:
-    default: false
-    description: "Optional icon on hover overlay"
+  flashOnSearch: true
+  flashScreenOnSearchHasNoMatch: true
+  showHoverOnOperate: false
   showHoverOnOperateIcon:
     default: 'icon'
     enum: ['none', 'icon', 'emoji']
-  showHoverSearchCounter:
-    default: false
+  showHoverSearchCounter: false
   showHoverSearchCounterDuration:
     default: 700
     description: "Duration(msec) for hover search counter"
-  hideTabBarOnMaximizePane:
-    default: true
+  hideTabBarOnMaximizePane: true
   smoothScrollOnFullScrollMotion:
     default: false
     description: "For `ctrl-f` and `ctrl-b`"


uncomment line

uncomment line

--- command-release.coffee
+++ command-release.coffee
@@ -27,7 +27,7 @@

 class Command
   constructor: (@config) ->
-    # process.on 'uncaughtException', @die
+    process.on 'uncaughtException', @die
     @githubService = new GithubService { @config }
     @gitService = new GitService { @config }


:racehorse: Avoid to constantly resolve user path

--- lib/snippets.coffee
+++ lib/snippets.coffee
@@ -13,6 +13,7 @@
   loaded: false

   activate: ->
+    @userSnippetsPath = null
     @scopedPropertyStore = new ScopedPropertyStore
     @subscriptions = new CompositeDisposable
     @subscriptions.add atom.workspace.addOpener (uri) =>
@@ -58,8 +59,11 @@
     atom.config.transact => @subscriptions.dispose()

   getUserSnippetsPath: ->
-    userSnippetsPath = CSON.resolve(path.join(atom.getConfigDirPath(), 'snippets'))
-    userSnippetsPath ? path.join(atom.getConfigDirPath(), 'snippets.cson')
+    return @userSnippetsPath if @userSnippetsPath?
+
+    @userSnippetsPath = CSON.resolve(path.join(atom.getConfigDirPath(), 'snippets'))
+    @userSnippetsPath ?= path.join(atom.getConfigDirPath(), 'snippets.cson')
+    @userSnippetsPath

   loadAll: (callback) ->
     @loadBundledSnippets (bundledSnippets) =>


move dev-code to end of file

move dev-code to end of file

--- 360.coffee
+++ 360.coffee
@@ -74,78 +74,6 @@
 # which we also need to support.
 #
 #{{{1 Literate source code
-#{{{2 Development code
-#
-# The following code is used during development.
-# It will automatically be removed when it is compiled and minified.
-#
-# The globalDefines sets `isTesting`, `isDevServer` and `isNodeJs` predicates which can be used for conditional code, ie. code present in the file used for test and development that will be removed from the production build.
-require("solapp").globalDefines global if typeof isNodeJs != "boolean"
-
-#{{{3 Meta information about the application
-# This is primarily used for the README.md and to make sure necessary css are included when the devserver is running, and also that the minified version `webjs` is build.
-if isNodeJs
-  exports.about =
-    title: "360º Viewer"
-    description: "Widget for showing OneTwo360 images/animations"
-    owner: "onetwo360"
-    name: "360"
-    html5:
-      userScaleable: true
-      css: [
-        "http://onetwo360.com/themes/onetwo360/site/360logofont.css"
-        "//netdna.bootstrapcdn.com/font-awesome/4.0.1/css/font-awesome.css"
-      ]
-    webjs: true
-    package:
-      dependencies:
-        solapp: "*"
-
-#{{{3 Test/experiment
-if isDevServer and !isNodeJs then do ->
-  sa = require "solapp"
-  exports.main = (solapp) ->
-    # actual htmlcontent, defined as json
-    solapp.setContent ["div"
-      ["center"
-          style:
-            width: 500
-            height: 500
-        ["span#threesixtyproduct", {style: {background: "#ccc"}}]]]
-    # invoke the threesixty component
-    onetwo360
-      elem_id: "threesixtyproduct"
-      product_id: "lukub2ip"
-      request_width: 600
-      request_height: 400
-
-  setTimeout (->
-    blah = document.createElement "div"
-    document.body.appendChild blah
-    blah.innerHTML = Date.now()
-    setInterval (->
-      blah.innerHTML = "#{window.innerHeight} #{window.innerWidth} #{body.scrollTop} #{body.scrollLeft}"
-    ), 1000
-
-    sleep 3, ->
-      njn()
-  ), 0
-
-#{{{3 REST server for logging
-if isNodeJs then do ->
-  exports.devServerMain = (app) ->
-    app.use "/logger", (req, res, next) ->
-      data = ""
-      req.on "data", (d) -> data += d
-      req.on "end", ->
-        res.header 'Access-Control-Allow-Origin', "*"
-        res.header 'Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'
-        res.header 'Access-Control-Allow-Headers', 'Content-Type'
-        console.log data
-        res.json {ok:true}
-        res.end()
-
-
 #{{{2 Utilities
 if !isNodeJs
   # General Utility functions {{{3
@@ -664,3 +592,75 @@
             elem.style.zoom = scaleFactor
         updateImage()
         false
+#{{{2 Development code
+#
+# The following code is used during development.
+# It will automatically be removed when it is compiled and minified.
+#
+# The globalDefines sets `isTesting`, `isDevServer` and `isNodeJs` predicates which can be used for conditional code, ie. code present in the file used for test and development that will be removed from the production build.
+require("solapp").globalDefines global if typeof isNodeJs != "boolean"
+
+#{{{3 Meta information about the application
+# This is primarily used for the README.md and to make sure necessary css are included when the devserver is running, and also that the minified version `webjs` is build.
+if isNodeJs
+  exports.about =
+    title: "360º Viewer"
+    description: "Widget for showing OneTwo360 images/animations"
+    owner: "onetwo360"
+    name: "360"
+    html5:
+      userScaleable: true
+      css: [
+        "http://onetwo360.com/themes/onetwo360/site/360logofont.css"
+        "//netdna.bootstrapcdn.com/font-awesome/4.0.1/css/font-awesome.css"
+      ]
+    webjs: true
+    package:
+      dependencies:
+        solapp: "*"
+
+#{{{3 Test/experiment
+if isDevServer and !isNodeJs then do ->
+  sa = require "solapp"
+  exports.main = (solapp) ->
+    # actual htmlcontent, defined as json
+    solapp.setContent ["div"
+      ["center"
+          style:
+            width: 500
+            height: 500
+        ["span#threesixtyproduct", {style: {background: "#ccc"}}]]]
+    # invoke the threesixty component
+    onetwo360
+      elem_id: "threesixtyproduct"
+      product_id: "lukub2ip"
+      request_width: 600
+      request_height: 400
+
+  setTimeout (->
+    blah = document.createElement "div"
+    document.body.appendChild blah
+    blah.innerHTML = Date.now()
+    setInterval (->
+      blah.innerHTML = "#{window.innerHeight} #{window.innerWidth} #{body.scrollTop} #{body.scrollLeft}"
+    ), 1000
+
+    sleep 3, ->
+      njn()
+  ), 0
+
+#{{{3 REST server for logging
+if isNodeJs then do ->
+  exports.devServerMain = (app) ->
+    app.use "/logger", (req, res, next) ->
+      data = ""
+      req.on "data", (d) -> data += d
+      req.on "end", ->
+        res.header 'Access-Control-Allow-Origin', "*"
+        res.header 'Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'
+        res.header 'Access-Control-Allow-Headers', 'Content-Type'
+        console.log data
+        res.json {ok:true}
+        res.end()
+
+


Drag: fix require

Drag: fix require

--- components/Drag.coffee
+++ components/Drag.coffee
@@ -1,5 +1,5 @@
 noflo = require 'noflo'
-Hammer = require 'hammer'
+Hammer = require 'hammerjs'

 class Drag extends noflo.Component
   description: 'Listen to drag events on a DOM element'


very basic lazy view impl

very basic lazy view impl

--- lib/ui/views.coffee
+++ lib/ui/views.coffee
@@ -22,6 +22,9 @@
     @ink.tree.treeView(@render(head),
                        children.map((x)=>@render @tags.div [x]),
                        expand: expand)
+
+  lazy: ({head, id}) ->
+    @ink.tree.treeView(@render(head),[])

   subtree: ({label, child}) ->
     @render if child.type == "tree"
@@ -88,6 +91,7 @@
     dom:     (a...) -> views.dom  a...
     html:    (a...) -> views.html a...
     tree:    (a...) -> views.tree a...
+    lazy:    (a...) -> views.lazy a...
     subtree: (a...) -> views.subtree a...
     link:    (a...) -> views.link a...
     copy:    (a...) -> views.copy a...


upgrade wakatime cli to v4.1.2

upgrade wakatime cli to v4.1.2

--- lib/wakatime.coffee
+++ lib/wakatime.coffee
@@ -14,7 +14,7 @@
 request = require 'request'
 rimraf = require 'rimraf'

-latestCLIVersion = '4.1.1'
+latestCLIVersion = '4.1.2'

 packageVersion = null
 unloadHandler = null


fix(search): Escape search query properly

--- src/flux/attributes/matcher.coffee
+++ src/flux/attributes/matcher.coffee
@@ -208,8 +208,13 @@
     return "NOT (" + wheres.join(" AND ") + ")"

 class SearchMatcher extends Matcher
-  constructor: (@searchQuery) ->
+  constructor: (searchQuery) ->
     super(null, null, null)
+    @searchQuery = (
+      searchQuery.trim()
+      .replace(/^['"]/, "")
+      .replace(/['"]$/, "")
+    )
     @

   attribute: =>


fix(search): Escape single quotes correctly

--- src/flux/attributes/matcher.coffee
+++ src/flux/attributes/matcher.coffee
@@ -1,5 +1,10 @@
 _ = require 'underscore'
 {tableNameForJoin} = require '../models/utils'
+
+# https://www.sqlite.org/faq.html#q14
+# That's right. Two single quotes in a row…
+singleQuoteEscapeSequence = "''"
+

 ###
 Public: The Matcher class encapsulates a particular comparison clause on an {Attribute}.
@@ -87,11 +92,6 @@
         return false

   whereSQL: (klass) ->
-
-    # https://www.sqlite.org/faq.html#q14
-    # That's right. Two single quotes in a row…
-    singleQuoteEscapeSequence = "''"
-
     if @comparator is "like"
       val = "%#{@val}%"
     else
@@ -214,6 +214,7 @@
       searchQuery.trim()
       .replace(/^['"]/, "")
       .replace(/['"]$/, "")
+      .replace(/'/g, singleQuoteEscapeSequence)
     )
     @


fix(extensions): Add name of extension incorrectly applying transforms

--- src/flux/stores/draft-store.coffee
+++ src/flux/stores/draft-store.coffee
@@ -413,7 +413,7 @@
                 console.log(transformed.body)
                 console.log("-- UNTRANSFORMED (should match BEFORE) --")
                 console.log(untransformed.body)
-                NylasEnv.reportError(new Error("An extension applied a tranform to the draft that it could not reverse."))
+                NylasEnv.reportError(new Error("Extension #{ext.name} applied a tranform to the draft that it could not reverse."))
               draft = transformed

       .then =>