Teamwork: Implemented fetching playgrounds from a remote manifest file.

--- client/app/Applications/Teamwork.kdapplication/Views/teamworkapp.coffee
+++ client/app/Applications/Teamwork.kdapplication/Views/teamworkapp.coffee
@@ -28,10 +28,7 @@
           title           : "Playgrounds"
           itemClass       : KDButtonViewWithMenu
           cssClass        : "clean-gray playgrounds-button"
-          menu            :
-            Facebook      :
-              callback    : (panel, workspace) =>
-                @handlePlaygroundSelection "Facebook"
+          menu            : []
         ]
         floatingPanes     : [ "chat" , "terminal", "preview" ]
         layout            :
@@ -52,6 +49,25 @@

     if options.playground
       @handlePlaygroundSelection options.playground
+    else
+      @teamwork.on "PanelCreated", =>
+        @doCurlRequest "https://raw.github.com/koding/Teamwork/master/Playgrounds/manifest.json", (err, manifest) =>
+          @populatePlaygroundsButton manifest
+
+  populatePlaygroundsButton: (playgrounds) ->
+    button   = @teamwork.getActivePanel().headerButtons.Playgrounds
+    menu     = []
+
+    playgrounds.forEach (playground) =>
+      item   = {}
+      {name} = playground
+      item[name] = {}
+      item[name].callback = =>
+        @handlePlaygroundSelection name, playground.manifestUrl
+
+      menu.push item
+
+    button.setOption "menu", menu

   showToolsModal: (panel, workspace) ->
     modal       = new KDModalView
@@ -181,9 +197,8 @@

     return rawOptions

-  handlePlaygroundSelection: (playground) ->
-    manifestPath = "https://raw.github.com/fatihacet/TeamworkPlaygrounds/master/#{playground}.json"
-    @doCurlRequest manifestPath, (err, manifest) =>
+  handlePlaygroundSelection: (playground, manifestUrl) ->
+    @doCurlRequest manifestUrl, (err, manifest) =>
       @teamwork.startNewSession @mergePlaygroundOptions manifest, playground
       @teamwork.container.setClass playground
       @teamwork.on "WorkspaceSyncedWithRemote", =>


updated docs

updated docs

--- assets/js/vo/menu-item-vo.coffee
+++ assets/js/vo/menu-item-vo.coffee
@@ -3,7 +3,8 @@
 # ```
 # {
 #   "title": "Getting started",
-#   "href": "/"
+#   "href": "/",
+#   "target": "_self" # optional
 # }
 # ```
 #


Reference lastBuildId instead of lastBuild when checking for existance

Referencing lastBuildId will not trigger loading a build record, which
will lower ajax queries number.

--- assets/scripts/app/routes.coffee
+++ assets/scripts/app/routes.coffee
@@ -124,7 +124,7 @@
   repoDidLoad: ->
     # TODO: it would be nicer to do it with promises
     repo = @controllerFor('repo').get('repo')
-    if repo && repo.get('isLoaded') && !repo.get('lastBuild')
+    if repo && repo.get('isLoaded') && !repo.get('lastBuildId')
       Ember.run.next =>
         @render('builds/not_found', outlet: 'pane', into: 'repo')


updated new_home in zh-HANS.coffee

updated new_home in zh-HANS.coffee

--- app/locale/zh-HANS.coffee
+++ app/locale/zh-HANS.coffee
@@ -51,7 +51,7 @@
 #    game_based: "When game-based learning systems are"
 #    compared: "compared"
 #    conventional: "against conventional assessment methods, the difference is clear: games are better at helping students retain knowledge, concentrate and"
-    perform_at_higher_level: "perform at a higher level of achievement"
+#    perform_at_higher_level: "perform at a higher level of achievement"
     feedback: "游戏同时提供实时反馈，使得学生调整他们的解决方法，并且从整体理解概念，而不是局限于“正确”或者“错误”的答案。"
     real_game: "一个真正的游戏，用真正的代码来玩耍。"
     great_game: "一个伟大的游戏不仅仅是勋章和成就。它关于玩家的旅程，设计良好的关卡，以及用帮助和信心迎接挑战的能力。"


Show stack error

Show stack error

--- lib/zen.coffee
+++ lib/zen.coffee
@@ -120,7 +120,11 @@
         console.log " ✓".green, "shutdown ok!".grey
         ZEN.br()
       process.on "uncaughtException", (error) =>
-        console.log " ⚑  ERR".red, "uncaughtException:", error.message.grey
+        stack1 = error.stack.indexOf " at "
+        stack2 = error.stack.indexOf " at ", stack1 + 1
+        file = error.stack.slice stack1, stack2
+        console.log(" ⚑  ERR".red, "uncaughtException:", __date(new Date()),
+          error.message.grey, file)
         process.exit 1
       process.on "SIGTERM", =>
         @server.close -> process.exit 1
@@ -201,6 +205,9 @@

 # -- Private methods -----------------------------------------------------------
 __time = (value) -> (new Date(value)).getTime()
+
+__date = (value) ->
+  [(value.getDate()), (value.getMonth()+1), value.getFullYear()].join '/'

 __server = ->
   if ZEN.protocol is "https"


Added tests for issue 95

Added tests for issue 95

--- test/knockback/collection-observable/test.coffee
+++ test/knockback/collection-observable/test.coffee
@@ -725,4 +725,41 @@
     kb.release(collection_observable)

     assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null
-    done()+    done()
+
+  it '19. push and unshift', (done) ->
+    kb.statistics = new kb.Statistics() # turn on stats
+
+    class PersonViewModel extends kb.ViewModel
+    collection_observable = kb.collectionObservable(new kb.ContactsCollection(), {view_model: PersonViewModel})
+
+    collection_observable.collection().push()
+    assert.equal(collection_observable.collection().length, 0)
+    assert.equal(collection_observable().length, 0)
+    collection_observable.collection().push(null)
+    assert.equal(collection_observable.collection().length, 0)
+    assert.equal(collection_observable().length, 0)
+    collection_observable.collection().push(new kb.Contact())
+    assert.equal(collection_observable.collection().length, 1)
+    assert.equal(collection_observable().length, 1)
+    collection_observable.collection().reset()
+    assert.equal(collection_observable.collection().length, 0)
+    assert.equal(collection_observable().length, 0)
+
+    collection_observable.collection().unshift()
+    assert.equal(collection_observable.collection().length, 0)
+    assert.equal(collection_observable().length, 0)
+    collection_observable.collection().unshift(null)
+    assert.equal(collection_observable.collection().length, 0)
+    assert.equal(collection_observable().length, 0)
+    collection_observable.collection().unshift(new kb.Contact())
+    assert.equal(collection_observable.collection().length, 1)
+    assert.equal(collection_observable().length, 1)
+    collection_observable.collection().reset()
+    assert.equal(collection_observable.collection().length, 0)
+    assert.equal(collection_observable().length, 0)
+
+    kb.release(collection_observable)
+
+    assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null
+    done()


missed some row property to int conversions

--- library.coffee
+++ library.coffee
@@ -62,7 +62,7 @@
     if isIgnoreSheet name
       dump "ignore sheet %s:'%s' rows %s.", sid, name, rows
       continue
-    crows = properties.getProperty name
+    crows = +properties.getProperty name
     dump "sheet %s:'%s' rows %s/%s.", sid, name, crows, rows
   dump "triggers %s installed for %s", spreadsheet.getName(),
     ScriptApp.getProjectTriggers().map (item) ->
@@ -172,7 +172,7 @@
 edit = (e) ->
   sheet = e.source.getActiveSheet()
   sheetName = sheet.getSheetName()
-  cachedRows = properties.getProperty sheetName
+  cachedRows = +properties.getProperty sheetName
   rows = sheet.getMaxRows()
   dump "onEdit range %s, name %s, event %s, rows %s, cached %s",
     e.range.getA1Notation(), sheetName, e, rows, cachedRows


Handle multi-line labels

Handle multi-line labels

--- src/label.coffee
+++ src/label.coffee
@@ -24,23 +24,28 @@
     # which can then be reused here
     Arcadia = require './arcadia.coffee'

-    context = @canvas.getContext '2d'
+    context = @canvas.getContext('2d')
+
+    lineCount = 1
+    newlines = @text.match(/\n/g)
+    lineCount = newlines.length + 1 if newlines

     # Determine width/height of text using offscreen

     element = document.getElementById 'text-dimensions'

     if !element
-      element = document.createElement 'div'
+      element = document.createElement('div')
       element['id'] = 'text-dimensions'
       element.style['position'] = 'absolute'
       element.style['top'] = '-9999px'
-      document.body.appendChild element
+      document.body.appendChild(element)

     element.style['font'] = @font
     element.style['line-height'] = 1
-    element.innerHTML = @text
+    element.innerHTML = @text.replace(/\n/g, '
').replace(/\s/g, ' ')
     @size.width = element.offsetWidth
     @size.height = element.offsetHeight
+    lineHeight = @size.height / lineCount
     @anchor = { x: @size.width / 2, y: @size.height / 2 }

     @canvas.width = @size.width + @_border.width + Math.abs(@_shadow.x) + @_shadow.blur
@@ -52,7 +57,6 @@

     @setAnchorPoint()

-    # Debug
     if @debug
       context.lineWidth = 1
       context.strokeStyle = '#f00'
@@ -70,7 +74,11 @@

     if @_color
       context.fillStyle = @_color
-      context.fillText @text, 0, 0, Arcadia.WIDTH
+      if lineCount > 1
+        @text.split('\n').forEach (text, index) =>
+          context.fillText(text, 0, -@size.height / 2 + lineHeight / 2 + (lineHeight * index))
+      else
+        context.fillText(@text, 0, 0)

     # Don't allow border to cast a shadow
     if @_shadow.x != 0 or @_shadow.y != 0 or @_shadow.blur != 0
@@ -81,7 +89,13 @@
     if @_border.width && @_border.color
       context.lineWidth = @_border.width
       context.strokeStyle = @_border.color
-      context.strokeText @text, 0, 0, Arcadia.WIDTH
+      if lineCount > 1
+        @text.split('\n').forEach (text, index) =>
+          context.strokeText(text, 0, -@size.height / 2 + lineHeight / 2 + (lineHeight * index))
+      else
+        context.strokeText(@text, 0, 0)
+
+    @dirty = false

   ###
   @description Getter/setter for font
@@ -102,7 +116,7 @@
   @property 'text',
     get: -> return @_text
     set: (value) ->
-      @_text = value
+      @_text = String(value)
       @dirty = true

   @property 'alignment',


comments

comments

--- app/scripts/editor.coffee
+++ app/scripts/editor.coffee
@@ -210,6 +210,33 @@
 		@checkEmpty()
 		if @status.empty
 			if clear then @clear() else @displayPrompt()
+	checkNew: ->
+		if $('#editor').text().length > 5
+			@status.new = false
+			$('#editor').trigger 'newPostCallback'
+	checkTitleEmpty: ->
+		@status.titleEmpty = $('#editor-title').text() == '' || $('#editor-title').text() == @titlePromptMessage
+	checkEmpty: ->
+		@status.empty = $('#editor').text() == '' || $('#editor').text() == @promptMessage
+	toggleFormatBlock: (jqel, tag)->
+		if jqel.attr 'name'
+			@saveSelection()
+			if jqel.is tag
+				newEl = $('<p>').html jqel.html()
+				newEl.attr 'name', jqel.attr 'name'
+				newEl.attr 'class', jqel.attr 'class'
+				jqel.after(newEl)
+				jqel.remove()
+				@restoreSelection()
+			else
+				newEl = $("<#{tag}>").html jqel.html()
+				newEl.attr 'name', jqel.attr 'name'
+				newEl.attr 'class', jqel.attr 'class'
+				jqel.after(newEl)
+				jqel.remove()
+				@restoreSelection()
+		else
+			document.execCommand('formatBlock', false, tag)

 	## EVENT BINDINGS ##
 	delegateEvents: ()->
@@ -273,35 +300,6 @@
 					range.selectNode node[0]
 					self.linkRange = range
 				self.applyOrCancelUrl self.linkRange, '' unless $(@).val()
-
-
-	checkNew: ->
-		if $('#editor').text().length > 5
-			@status.new = false
-			$('#editor').trigger 'newPostCallback'
-	checkTitleEmpty: ->
-		@status.titleEmpty = $('#editor-title').text() == '' || $('#editor-title').text() == @titlePromptMessage
-	checkEmpty: ->
-		@status.empty = $('#editor').text() == '' || $('#editor').text() == @promptMessage
-	toggleFormatBlock: (jqel, tag)->
-		if jqel.attr 'name'
-			@saveSelection()
-			if jqel.is tag
-				newEl = $('<p>').html jqel.html()
-				newEl.attr 'name', jqel.attr 'name'
-				newEl.attr 'class', jqel.attr 'class'
-				jqel.after(newEl)
-				jqel.remove()
-				@restoreSelection()
-			else
-				newEl = $("<#{tag}>").html jqel.html()
-				newEl.attr 'name', jqel.attr 'name'
-				newEl.attr 'class', jqel.attr 'class'
-				jqel.after(newEl)
-				jqel.remove()
-				@restoreSelection()
-		else
-			document.execCommand('formatBlock', false, tag)
 	handleTitleKeyDown: (e)->
 		switch e.keyCode
 			when 13


update href when keydown

update href when keydown

--- app/scripts/editor.coffee
+++ app/scripts/editor.coffee
@@ -193,9 +193,13 @@
 			if self.linkRange
 				a = document.createElement 'a'
 				a.setAttribute 'href', '#'
+				self.lastLinkElement = a
 				self.linkRange.surroundContents a
 		.delegate 'li:first-child', 'mouseenter', (e)->
 			self.linkRange = self.selection().getRangeAt 0
+		.delegate 'input', 'keydown', (e)->
+			if self.lastLinkElement
+				self.lastLinkElement.setAttribute 'href', $(@).val()

 		$('#toolpad').delegate '.cancel', 'click', (e)=>
 			@cancelLinkInput()


refactor

refactor

--- app/scripts/editor.coffee
+++ app/scripts/editor.coffee
@@ -11,6 +11,25 @@
 		new: true
 		connecting: false
 		changed: false
+	constructor: (options={})->
+		@id = options.id
+		@status.new = !@id
+		@newPostCallback = options.newPostCallback
+		@autosaveCallback = options.autosaveCallback
+		@articleCreateURL = options.articleCreateURL
+		@articleSaveURL = options.articleSaveURL
+		@articleDeleteURL = options.articleDeleteURL
+	init: ()->
+		self = @
+		$('#editor').html '

' if @status.new
+		@autosave 5000
+		@update()
+		@checkTitleEmpty()
+		@checkEmpty()
+		@displayTitlePrompt() if @status.titleEmpty
+		@displayPrompt() if @status.empty
+		@bindEditorTitleEvents()
+		@bindEditorEvents()
 	setConnecting: (bool)->
 		@status.connecting = bool
 	setChanged: (bool)->
@@ -25,6 +44,29 @@
 		$('#editor').children().first().html('
')
 	clearTitle: ->
 		$('#editor-title').html ''
+	clearStatus: ->
+		$('.debug-status').addClass 'hidden'
+		@status.cmd = @status.ctrl = @status.alt = @status.shift = false
+	cleanAttributes: (el)->
+		attributes = $.map el.attributes, (item)->
+			item.name
+		$.each attributes, (i, key)=>
+			$(@).removeAttr(key)
+	divsToPs: ->
+		#transform divs to ps
+		@saveSelection()
+		$('#editor div').not('[name]').each ->
+			el = $('
')
+			el.html $(@).html()
+			$(@).after(el)
+			$(@).remove()
+		@restoreSelection()
+	assignNameAttribute: (jqel)->
+		jqel.attr 'name', @rand()
+	rand: (len=4)->
+		result = ''
+		result += Math.random().toString(36).substr(2,1) for i in new Array(len)
+		result.toUpperCase()
 	detectChanged: ->
 			title = $('#editor-title').html()
 			content = $('#editor').html()
@@ -47,7 +89,6 @@
 				cursorEnd.id = 'cursorEnd'
 				range.collapse()
 				range.insertNode cursorEnd
-
 	restoreSelection: ->
 		cursorStart = document.getElementById 'cursorStart'
 		cursorEnd = document.getElementById 'cursorEnd'
@@ -67,13 +108,7 @@
 	getSelectedElement: ->
 		el = $(@selection().getRangeAt(0).commonAncestorContainer)
 		return if el[0].nodeType == 3 then el.parent() else el
-	clearStatus: ->
-		$('.debug-status').addClass 'hidden'
-		@status.cmd = @status.ctrl = @status.alt = @status.shift = false
-	rand: (len=4)->
-		result = ''
-		result += Math.random().toString(36).substr(2,1) for i in new Array(len)
-		result.toUpperCase()
+
 	autosave: (durationInMilliseconds=5000)->
 		(update= =>
 			setTimeout =>
@@ -82,22 +117,7 @@
 					@autosaveCallback(@) if @autosaveCallback
 				update()
 			, durationInMilliseconds)()
-	divsToPs: ->
-		#transform divs to ps
-		@saveSelection()
-		$('#editor div').not('[name]').each ->
-			el = $('
')
-			el.html $(@).html()
-			$(@).after(el)
-			$(@).remove()
-		@restoreSelection()
-	cleanAttributes: (el)->
-		attributes = $.map el.attributes, (item)->
-			item.name
-		$.each attributes, (i, key)=>
-			$(@).removeAttr(key)
-	assignNameAttribute: (jqel)->
-		jqel.attr 'name', @rand()
+
 	showInsertion: ->
 		$('#insertion').removeClass 'hidden'
 	hideInsertion: ->
@@ -258,26 +278,6 @@
 			when 91
 				@status.cmd = false
 				$('.cmd').addClass('hidden')
-	constructor: (options={})->
-		@id = options.id
-		@status.new = !@id
-		@newPostCallback = options.newPostCallback
-		@autosaveCallback = options.autosaveCallback
-		@articleCreateURL = options.articleCreateURL
-		@articleSaveURL = options.articleSaveURL
-		@articleDeleteURL = options.articleDeleteURL
-
-	init: ()->
-		self = @
-		$('#editor').html '

' if @status.new
-		@autosave 5000
-		@update()
-		@checkTitleEmpty()
-		@checkEmpty()
-		@displayTitlePrompt() if @status.titleEmpty
-		@displayPrompt() if @status.empty
-		@bindEditorTitleEvents()
-		@bindEditorEvents()

 	bindEditorTitleEvents:->
 		$('#editor-title').on 'keydown', (e)=>


do not save if it is connecting

do not save if it is connecting

--- app/scripts/editor.coffee
+++ app/scripts/editor.coffee
@@ -68,7 +68,7 @@
 	autosave: (durationInMilliseconds=5000)->
 		(update= =>
 			setTimeout =>
-				if @id && @status.changed
+				if @id && @status.changed && @status.connecting
 					@setChanged false
 					@autosaveCallback() if @autosaveCallback
 				update()


cancel auto link

cancel auto link

--- app/scripts/editor.coffee
+++ app/scripts/editor.coffee
@@ -147,9 +147,6 @@
 				@showToolpadOverSelection 'toolpad'
 				@restoreSelection 'toolpad'
 				@linkRange = @selection().getRangeAt 0
-				unless @getSelectedElement().is 'a'
-					@applyOrCancelUrl @linkRange, '#'
-					@linkRange = @selection().getRangeAt 0
 			else
 				@hideToolpad()
 	showToolpadOverSelection: (prefix='m')->


check the article is new or not

check the article is new or not

--- app/scripts/editor.coffee
+++ app/scripts/editor.coffee
@@ -6,6 +6,7 @@
 		alt: false
 		shift: false
 		empty: false
+		new: true
 	displayPrompt: ->
 		$('#editor').html @promptMessage
 	clear: ->
@@ -57,6 +58,8 @@
 		$('p,h1,h2,pre').not('[name]').each ->
 			$(@).attr 'name', self.rand()
 		$('#debug').text $('#editor').html()
+	checkNew: ->
+		@status.new = false if $('#editor').text().length > 5
 	checkEmpty: ->
 		@status.empty = $('#editor').text() == '' || $('#editor').html() == @promptMessage
 	toggleFormatBlock: (tag)->
@@ -129,6 +132,7 @@

 		# binding
 		$('#editor').on 'keydown', (e)=>
+			@checkNew() if @status.new
 			@handleKeyDown(e)
 			@update()
 		.on 'keyup', (e)=>


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/number_extensions.coffee
+++ src/number_extensions.coffee
@@ -74,7 +74,7 @@
   Math.round(this)

 ###*
-Get a bunch of points eq
+Get a bunch of points equally spaced

 4.circularPoints (p) ->


Workaround for pre 1.3 CS no longer necessary

--- src/model.coffee
+++ src/model.coffee
@@ -29,10 +29,6 @@
         fromCache = Cache.get(@constructor, attributes.id)
         if fromCache
           fromCache.set(attributes)
-          # temporary workaround until CS bug is fixed
-          # otherwise, we neither return the cached object
-          # nor set attributes on the new one
-          @set(attributes)
           return fromCache
         else
           Cache.set(@constructor, attributes.id, this)


functions need to be pure

functions need to be pure

--- src/nodes.coffee
+++ src/nodes.coffee
@@ -1050,8 +1050,11 @@
       return [@makeCode "[#{ range.join(', ') }]"]

     @warn "using _.range(); will not work if left hand side < right"
-    @to = new Op('+', @to, new Literal('1')) unless @exclusive
-    u_range_call = new Call(mkVanillaID('_.range'), [@from, @to])
+    unless @exclusive
+      to = new Op('+', @to, new Literal('1'))
+    else
+      to = @to
+    u_range_call = new Call(mkVanillaID('_.range'), [@from, to])
     return u_range_call.compileNode o

 #### Slice


Check for invalid implicit values

Check for invalid implicit values

--- src/nodes.coffee
+++ src/nodes.coffee
@@ -717,8 +717,11 @@

   compileNode: (o) ->
     props = @properties
-    props = props.filter ((x) -> x not instanceof Value) if @generated
     return (if @front then '({})' else '{}') unless props.length
+    if @generated
+      for node in props
+        if node instanceof Value
+          throw new Error 'No implicit values in implict objects allowed'
     idt         = o.indent += TAB
     lastNoncom  = @lastNonComment @properties
     props = for prop, i in props


Generic getNodes() and some() for containers.

--- src/range.coffee
+++ src/range.coffee
@@ -192,7 +192,7 @@
         origParent = node.parentNode

       path = xpath.fromNode(origParent, root)
-      textNodes = getTextNodes(origParent)
+      textNodes = getNodes(origParent, isTextNode)

       # Calculate real offset as the combined length of all the
       # preceding textNode siblings. We include the length of the
@@ -229,7 +229,7 @@
   #
   # Returns an Array of TextNode instances.
   textNodes: ->
-    textNodes = getTextNodes(this.commonAncestor)
+    textNodes = getNodes(this.commonAncestor, isTextNode)
     [start, end] = [textNodes.indexOf(this.start), textNodes.indexOf(this.end)]
     # Return the textNodes that fall between the start and end indexes.
     return textNodes[start..end]
@@ -281,7 +281,7 @@
       # Target the string index of the last character inside the range.
       if p is 'end' then targetOffset -= 1

-      for tn in getTextNodes(node)
+      for tn in getNodes(node, isTextNode)
         if (length + tn.nodeValue.length > targetOffset)
           range[p + 'Container'] = tn
           range[p + 'Offset'] = this[p + 'Offset'] - length
@@ -322,6 +322,28 @@
     }

+some = (node, fn, traverse = preFirst) ->
+  result = []
+  while fn(node)
+    result.push(node)
+    node = traverse(node)
+  return result
+
+
+# Get all the matching Nodes contained by a root Node.
+getNodes = (root, fn = (-> true)) ->
+  result = []
+  last = lastLeaf(root)
+
+  collect = (node) ->
+    if fn(node) then result.push(node)
+    return node isnt last
+
+  some(root, collect)
+
+  return result
+
+
 # Split a TextNode at an offset, returning the successor.
 # https://github.com/Raynos/DOM-shim/issues/11
 splitText = (node, offset) ->
@@ -331,18 +353,6 @@
   return insertAfter(tail, node)

-# Get all the text Nodes within a Node.
-getTextNodes = (root) ->
-  text = []
-  node = root
-  end = lastLeaf(root)
-  while node = findNode(node, end, isTextNode, preFirst)
-    text.push(node)
-    if node is end then break
-    node = preFirst(node)
-  return text
-
-
 # Predicate for checking if a node is a TextNode.
 isTextNode = (node) ->
   return node.nodeType is TEXT_NODE


make remove for empty ids array behave correctly

--- src/redis.coffee
+++ src/redis.coffee
@@ -94,6 +94,11 @@
         unless 'function' is typeof cb
             throw new Error 'cb argument must be a function'

+        if ids.length is 0
+            process.nextTick ->
+                cb()
+            return
+
         prefix = config.keyPrefix or defaultKeyPrefix

         config.redis.eval lua.remove, 0, prefix, JSON.stringify(ids), cb


Fix #427

Fix #427

--- src/spine.coffee
+++ src/spine.coffee
@@ -105,22 +105,12 @@
   @toString: -> "#{@className}(#{@attributes.join(", ")})"

   @find: (id) ->
-    record = @records[id]
-    if !record and ("#{id}").match(/c-\d+/)
-      return @findCID(id)
-    throw new Error("\"#{@className}\" model could not find a record for the ID \"#{id}\"") unless record
+    throw new Error("\"#{@className}\" model could not find a record for the ID \"#{id}\"") unless @exists(id)
+    record = @records[id] or @crecords[id]
     record.clone()

-  @findCID: (cid) ->
-    record = @crecords[cid]
-    throw new Error("\"#{@className}\" model could not find a record for the ID \"#{id}\"") unless record
-    record.clone()
-
   @exists: (id) ->
-    try
-      return @find(id)
-    catch e
-      return false
+    id of @records or id of @crecords

   @refresh: (values, options = {}) ->
     if options.clear
@@ -346,7 +336,7 @@
     @load(result)

   exists: ->
-    @id && @id of @constructor.records
+    @constructor.exists(@id)

   # Private


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/state.coffee
+++ src/state.coffee
@@ -5,7 +5,7 @@
   self = GameObject().extend

     defaultState: () ->
-      return I.initialState
+      return initialState

     transition: (toState) ->
       return currentState = toState


Rename rootView to workspaceView

Rename rootView to workspaceView

--- lib/dev-live-reload.coffee
+++ lib/dev-live-reload.coffee
@@ -11,6 +11,6 @@
       packages = (k for k, __ of uiWatcher.watchedPackages)
       console.info 'dev-live-reload: Watching', themes, packages

-    atom.rootView.command 'dev-live-reload:reload-all', ->
+    atom.workspaceView.command 'dev-live-reload:reload-all', ->
       console.info 'dev-live-reload: Reloading all styles!'
       uiWatcher.reloadAll()


fixing accidental commit of merge conflict

--- jsapp/xlform_model_view/view.rowDetail.coffee
+++ jsapp/xlform_model_view/view.rowDetail.coffee
@@ -257,15 +257,15 @@

       @$el.addClass("card__settings__fields--active")
       if @model_is_group()
-        return viewRowDetail.Templates.dropdown @cid, @model.key, types.group, 'Appearance'
+        return viewRowDetail.Templates.checkbox @cid, @model.key, 'Appearance (advanced)', 'Show all questions in this group on the same screen'
       else
         appearances = types[@model._parent.getValue('type').split(' ')[0]]
         if appearances?
           appearances.push 'other'
           appearances.unshift 'select'
-          return viewRowDetail.Templates.dropdown @cid, @model.key, appearances, 'Appearance'
+          return viewRowDetail.Templates.dropdown @cid, @model.key, appearances, 'Appearance (advanced)'
         else
-          return viewRowDetail.Templates.textbox @cid, @model.key, 'Appearance', 'text'
+          return viewRowDetail.Templates.textbox @cid, @model.key, 'Appearance (advanced)', 'text'

     model_is_group: () ->
       @model._parent.constructor.key == 'group'


Reduce the data logged when updating ranges

--- app/coffee/RangesManager.coffee
+++ app/coffee/RangesManager.coffee
@@ -7,7 +7,6 @@

 	applyUpdate: (project_id, doc_id, entries = {}, updates = [], callback = (error, new_entries) ->) ->
 		{changes, comments} = entries
-		logger.log {changes, comments, updates}, "applying updates to ranges"
 		rangesTracker = new RangesTracker(changes, comments)
 		for update in updates
 			rangesTracker.track_changes = !!update.meta.tc
@@ -20,12 +19,12 @@
 			return callback new Error("too many comments or tracked changes")

 		response = RangesManager._getRanges rangesTracker
-		logger.log {response}, "applied updates to ranges"
+		logger.log {project_id, doc_id, changesCount: response.changes?.length, commentsCount: response.comments?.length}, "applied updates to ranges"
 		callback null, response

 	acceptChange: (change_id, ranges, callback = (error, ranges) ->) ->
 		{changes, comments} = ranges
-		logger.log {changes, comments, change_id}, "accepting change in ranges"
+		logger.log {change_id}, "accepting change in ranges"
 		rangesTracker = new RangesTracker(changes, comments)
 		rangesTracker.removeChangeId(change_id)
 		response = RangesManager._getRanges(rangesTracker)
@@ -33,7 +32,7 @@

 	deleteComment: (comment_id, ranges, callback = (error, ranges) ->) ->
 		{changes, comments} = ranges
-		logger.log {changes, comments, comment_id}, "deleting comment in ranges"
+		logger.log {comment_id}, "deleting comment in ranges"
 		rangesTracker = new RangesTracker(changes, comments)
 		rangesTracker.removeCommentId(comment_id)
 		response = RangesManager._getRanges(rangesTracker)


Revert "Try to make the tab key work in the JSON editor"

This reverts commit 884802ac6dde446473586c62c22f267389db77da.

--- app/components/json-editor.cjsx
+++ app/components/json-editor.cjsx
@@ -2,96 +2,12 @@

 # This thing is only temporary until we get the real workflow UI figured out.

-TAB = 9
-
-TAB_PLACEHOLDER = '\t{{TAB}}\t'
-
 module.exports = React.createClass
   displayName: 'JSONEditor'

-  getDefaultProps: ->
-    tabChars: '  '
-
   render: ->
-    style =
-      fontFamily: 'monospace'
-
     valid = try JSON.parse @props.value
     unless valid
-      style.outline = '2px dashed red'
+      style = outline: '2px solid red'

-    <textarea {...@props} className="json-editor" style={style} onKeyDown={@handleKeyDown}/>
-
-  handleKeyDown: (e) ->
-    if e.which is TAB and not e.altKey # Alt-tab will focus the next input as normal.
-      e.preventDefault()
-
-      lines = e.target.value.split '\n'
-
-      start = e.target.selectionStart;
-      end = e.target.selectionEnd;
-
-      startLine = e.target.value[0...start].split(/\n/).length - 1
-      endLine = e.target.value[0...end].split(/\n/).length - 1
-
-      console.log {start}, {end}, {startLine}, {endLine}
-
-      if start is end # No selection
-        if e.shiftKey
-          # Dedent current line
-          e.target.value = @dedent lines, [startLine]
-          e.target.selectionStart = start - @props.tabChars.length
-          e.target.selectionEnd = start - @props.tabChars.length
-        else
-          # Insert a tab
-          e.target.value = @replaceSelectionWithTab e.target.value, start, end
-          e.target.selectionStart = start + @props.tabChars.length
-          e.target.selectionEnd = start + @props.tabChars.length
-
-      else
-        if startLine is endLine # Selection within one line
-          if e.shiftKey
-            # Dedent current line
-            e.target.value = @dedent lines, [startLine]
-            e.target.selectionStart = start - @props.tabChars.length
-            e.target.selectionEnd = start - @props.tabChars.length
-          else
-            # Replace selection with a tab
-            e.target.value = @replaceSelectionWithTab e.target.value, start, end
-            e.target.selectionStart = start + @props.tabChars.length
-            e.target.selectionEnd = start + @props.tabChars.length
-        else
-          # Selection across lines
-          if e.target.value.charAt(end - 1) is '\n'
-            # Technically it's a different line, but nothing is selected.
-            endLine -= 1
-
-          linesInSelection = [startLine..endLine]
-
-          # TODO: Repositioning the selection here is very sloppy.
-
-          if e.shiftKey
-            e.target.value = @dedent lines, linesInSelection
-            if e.target.value.charAt(start - @props.tabChars.length) is '\n'
-              e.target.selectionStart = start
-            else
-              e.target.selectionStart = start - @props.tabChars.length
-            e.target.selectionEnd = end - (@props.tabChars.length * linesInSelection.length)
-          else
-            e.target.value = @indent lines, linesInSelection
-            e.target.selectionStart = start + @props.tabChars.length
-            e.target.selectionEnd = end + (@props.tabChars.length * linesInSelection.length)
-
-  replaceSelectionWithTab: (string, start, end) ->
-    "#{string[...start]}#{@props.tabChars}#{string[end...]}"
-
-  dedent: (lines, toDedent) ->
-    for i in toDedent
-      if lines[i].indexOf(@props.tabChars) is 0
-        lines[i] = lines[i].replace @props.tabChars, ''
-    lines.join '\n'
-
-  indent: (lines, toIndent) ->
-    for i in toIndent
-      lines[i] = @props.tabChars + lines[i]
-    lines.join '\n'
+    <textarea {...@props} className="json-editor" style={style} />


Add extension check to fix dropdown for sound schema (#331)

--- app/core/treema-ext.ozar.coffee
+++ app/core/treema-ext.ozar.coffee
@@ -127,8 +127,10 @@

     menu = $('

')
     files = @getFiles()
+    fileExtName = mimetype.replace('audio/', '')
     for file in files
-      continue unless file.get('contentType') in mimetypes
+      # contentType is no longer returned from the API due to s3 migration.
+      continue unless file.get('contentType') in mimetypes or file.get('filename').endsWith(fileExtName)
       path = file.get('metadata').path
       filename = file.get 'filename'
       fullPath = [path, filename].join('/')


Create the image renderer.

Create the image renderer.

--- app/javascripts/services.coffee
+++ app/javascripts/services.coffee
@@ -543,11 +543,12 @@
     # The image binary content.
     data: null

-    # Transfers the image file content to the data attribute.
+    # Transfers the image file content to the data property.
     # The image state loading flag is set to true while the
     # file is read.
     #
-    # @returns the Angular $http request
+    # @returns a promise which resolves to this image when
+    #   file content is loaded into the data property
     load: () ->
       # Set the loading flag.
       @state.loading = true
@@ -559,11 +560,46 @@
         @state.loading = false
         # Set the data property to the file content.
         @data = data
-
-    # Configures the XTK renderer for this Image.
-    configureRenderer: () ->
-      # TODO - Build the image config object.
-      # this.config = ...
+        # Return the image.
+        this
+
+    # Builds an XTK renderer for the given Image.
+    createRenderer: (image) ->
+      # The XTK renderer.
+      renderer = new X.renderer3D()
+
+      # The volume to render.
+      volume = new X.volume()
+      volume.file = image.filename
+      volume.filedata = image.data
+      renderer.add volume
+
+      # The rendering callback. This function is called after the
+      # volume is initialized and prior to the first rendering.
+      renderer.onShowtime = ->
+        # The volume display controls.
+        volumeCtls = new dat.GUI()
+        # The controls interact with the volume.
+        volumeCtls = gui.addFolder('Volume')
+        # The rendering control.
+        renderingCtl = volumeCtls.add(volume, 'volumeRendering')
+        # The opacity control.
+        opacityCtl = volumeCtls.add(volume, 'opacity', 0, 1).listen()
+        # The threshold min and max range controls.
+        minCtl = volumeCtls.add(volume, 'lowerThreshold', volume.min, volume.max)
+        maxCtl = volumeCtls.add(volume, 'upperThreshold', volume.min, volume.max)
+        # The slice dimension controls.
+        sliceXCtl = volumeCtls.add(volume, 'indexX', 0, volume.range[0] - 1)
+        sliceYCtl = volumeCtls.add(volume, 'indexY', 0, volume.range[1] - 1)
+        sliceZCtl = volumeCtls.add(volume, 'indexZ', 0, volume.range[2] - 1)
+        # Display the controls.
+        volumeCtls.open();
+
+      # Adjust the camera position.
+      renderer.camera.position = [120, 80, 160]
+
+      # Return the renderer.
+      renderer

   # Obtains image objects for the given ImageContainer. The image
   # object content is described in the create() function.


Up/down previous command scrolling works

--- lib/atom-slime-repl-view.coffee
+++ lib/atom-slime-repl-view.coffee
@@ -8,6 +8,9 @@
   prompt: "> "
   preventUserInput: false
   inputFromUser: true
+  # Keep track of command history, for use with the up/down arrows
+  previousCommands: []
+  cycleIndex: null

   constructor: (@swank) ->
     @prompt = @pkg + "> "
@@ -78,8 +81,6 @@
     @subs.add atom.commands.add @editorElement, 'editor:newline': (event) => @handleEnter(event)
     @subs.add atom.commands.add @editorElement, 'editor:newline-below': (event) => @handleEnter(event)

-
-
     @subs.add @editor.onWillInsertText (event) =>
       #console.log 'Insert: ' + event.text
       # console.log "Insert: #{event.text}"
@@ -87,6 +88,15 @@
       if @inputFromUser and (@preventUserInput or point.column < @prompt.length or point.row < @editor.getLastBufferRow())
         event.cancel()

+    # Set up up/down arrow previous command cycling
+    @subs.add atom.commands.add @editorElement, 'core:move-up': (event) =>
+      @cycleBack()
+      event.stopImmediatePropagation()
+    @subs.add atom.commands.add @editorElement, 'core:move-down': (event) =>
+      @cycleForward()
+      event.stopImmediatePropagation()
+
+
     @subs.add @editor.onDidDestroy =>
       @closeRepl()

@@ -115,7 +125,13 @@
     point = @editor.getCursorBufferPosition()
     if point.row == @editor.getLastBufferRow() and !@preventUserInput and @swank.connected
       input = @getUserInput()
+      # Push this command to the ring if applicable
+      if input != '' and @previousCommands[@previousCommands.length - 1] != input
+        @previousCommands.push input
+      @cycleIndex = @previousCommands.length
+
       @preventUserInput = true
+      @editor.moveToEndOfLine()
       @appendText("\n")
       promise = @swank.eval input, @pkg
       promise.then =>
@@ -123,6 +139,7 @@
         @preventUserInput = false
     # Stop the enter
     event.stopImmediatePropagation()
+

   insertPrompt: () ->
     @appendText "\n" + @prompt
@@ -148,6 +165,36 @@
       @closeDebugTab()

+  cycleBack: () ->
+    # Cycle back through command history
+    @cycleIndex = @cycleIndex - 1 if @cycleIndex > 0
+    @showPreviousCommand(@cycleIndex)
+
+
+  cycleForward: () ->
+    # Cycle forward through command history
+    @cycleIndex = @cycleIndex + 1 if @cycleIndex < @previousCommands.length
+    @showPreviousCommand(@cycleIndex)
+
+
+  showPreviousCommand: (index) ->
+    if index >= @previousCommands.length
+      # Empty it
+      @setPromptCommand ''
+    else if index >= 0 and index < @previousCommands.length
+      cmd = @previousCommands[index]
+      @setPromptCommand cmd
+
+
+  setPromptCommand: (cmd) ->
+    # Sets the command at the prompt
+    lastrow = @editor.getLastBufferRow()
+    lasttext = @editor.lineTextForBufferRow(lastrow)
+    range = new Range([lastrow, 0], [lastrow, lasttext.length])
+    newtext = "#{@prompt}#{cmd}"
+    @editor.setTextInBufferRange(range, newtext, undo:'skip')
+
+
   setupDebugger: () ->
     process.nextTick =>
     @subs.add atom.workspace.addOpener (filePath) =>
@@ -157,14 +204,17 @@
       if e.item == @dbgv
         @swank.debug_escape_all()

+
   createDebugTab: (obj) ->
     @dbgv = new DebuggerView
     @dbgv.setup(@swank, obj)

   showDebugTab: () ->
     @replPane.activate()
-    atom.workspace.open('slime://debug').then (editor) =>
-      # Debugger now opened
+    atom.workspace.open('slime://debug').then (d) =>
+      # TODO - doesn't work
+      #elt = atom.views.getView(d)
+      #atom.commands.add elt, 'q': (event) => @closeDebugTab()

   closeDebugTab: () ->
     @replPane.destroyItem(@dbgv)


:art: Simplify conditional

:art: Simplify conditional

--- lib/autocomplete-manager.coffee
+++ lib/autocomplete-manager.coffee
@@ -449,7 +449,7 @@
     # bufferChanged handler decides to show suggestions, it will cancel the
     # hideSuggestionList request. If there is no bufferChanged event,
     # suggestionList will be hidden.
-    @requestHideSuggestionList() if not textChanged and not @shouldActivate
+    @requestHideSuggestionList() unless textChanged or @shouldActivate

   # Private: Gets called when the user saves the document. Cancels the
   # autocompletion.


Add highlighting to custom pairs.

Add highlighting to custom pairs.

--- lib/bracket-matcher-view.coffee
+++ lib/bracket-matcher-view.coffee
@@ -3,28 +3,29 @@
 {Range} = require 'atom'
 TagFinder = require './tag-finder'

-startPairMatches =
-  '(': ')'
-  '[': ']'
-  '{': '}'
-
-endPairMatches =
-  ')': '('
-  ']': '['
-  '}': '{'
-
-pairRegexes = {}
-for startPair, endPair of startPairMatches
-  pairRegexes[startPair] = new RegExp("[#{_.escapeRegExp(startPair + endPair)}]", 'g')
-
 module.exports =
 class BracketMatcherView
+  startPairMatches:
+    '(': ')'
+    '[': ']'
+    '{': '}'
+
+  endPairMatches:
+    ')': '('
+    ']': '['
+    '}': '{'
+
   constructor: (@editor, editorElement) ->
     @subscriptions = new CompositeDisposable
     @tagFinder = new TagFinder(@editor)
     @pairHighlighted = false
     @tagHighlighted = false

+    @pairRegexes = {}
+    @updatePairs()
+    for startPair, endPair of @startPairMatches
+      @pairRegexes[startPair] = new RegExp("[#{_.escapeRegExp(startPair + endPair)}]", 'g')
+
     # TODO: remove conditional when `onDidChangeText` ships on stable.
     if typeof @editor.getBuffer().onDidChangeText is "function"
       @subscriptions.add @editor.getBuffer().onDidChangeText =>
@@ -60,6 +61,31 @@
   destroy: =>
     @subscriptions.dispose()

+  excludePairs: (excludePairs) ->
+    if excludePairs.length
+      for excludePair in excludePairs
+        pairArray = excludePair.split ':'
+        @startPairMatches = _.omit(@startPairMatches, pairArray[0])
+        @endPairMatches = _.omit(@endPairMatches, pairArray[1])
+
+  addPairs: (addPairs) ->
+    if addPairs.length
+      for addPair in addPairs
+        pairArray = addPair.split ':'
+        newStartPair = {}
+        newStartPair[pairArray[0]] = pairArray[1]
+        @startPairMatches = _.extend(@startPairMatches, newStartPair)
+        newEndPair = {}
+        newEndPair[pairArray[1]] = pairArray[0]
+        @endPairMatches = _.extend(@endPairMatches, newEndPair)
+
+  updatePairs: () ->
+    @excludePairs(@getScopedSetting('bracket-matcher.excludePairs'))
+    @addPairs(@getScopedSetting('bracket-matcher.addPairs'))
+
+  getScopedSetting: (key) ->
+    atom.config.get(key, scope: @editor.getLastCursor().getScopeDescriptor())
+
   subscribeToCursor: ->
     cursor = @editor.getLastCursor()
     return unless cursor?
@@ -84,11 +110,11 @@
     return unless @editor.getLastSelection().isEmpty()
     return if @editor.isFoldedAtCursorRow()

-    {position, currentPair, matchingPair} = @findCurrentPair(startPairMatches)
+    {position, currentPair, matchingPair} = @findCurrentPair(@startPairMatches)
     if position
       matchPosition = @findMatchingEndPair(position, currentPair, matchingPair)
     else
-      {position, currentPair, matchingPair} = @findCurrentPair(endPairMatches)
+      {position, currentPair, matchingPair} = @findCurrentPair(@endPairMatches)
       if position
         matchPosition = @findMatchingStartPair(position, matchingPair, currentPair)

@@ -111,12 +137,12 @@
       text = @editor.getSelectedText()

       #check if the character to the left is part of a pair
-      if startPairMatches.hasOwnProperty(text) or endPairMatches.hasOwnProperty(text)
-        {position, currentPair, matchingPair} = @findCurrentPair(startPairMatches)
+      if @startPairMatches.hasOwnProperty(text) or @endPairMatches.hasOwnProperty(text)
+        {position, currentPair, matchingPair} = @findCurrentPair(@startPairMatches)
         if position
           matchPosition = @findMatchingEndPair(position, currentPair, matchingPair)
         else
-          {position, currentPair, matchingPair} = @findCurrentPair(endPairMatches)
+          {position, currentPair, matchingPair} = @findCurrentPair(@endPairMatches)
           if position
             matchPosition = @findMatchingStartPair(position, matchingPair, currentPair)

@@ -125,7 +151,7 @@
           @editor.delete()
           # if on the same line and the cursor is in front of an end pair
           # offset by one to make up for the missing character
-          if position.row is matchPosition.row and endPairMatches.hasOwnProperty(currentPair)
+          if position.row is matchPosition.row and @endPairMatches.hasOwnProperty(currentPair)
             position = position.traverse([0, -1])
           @editor.setCursorBufferPosition(position)
           @editor.delete()
@@ -138,7 +164,7 @@
     scanRange = new Range(startPairPosition.traverse([0, 1]), @editor.buffer.getEndPosition())
     endPairPosition = null
     unpairedCount = 0
-    @editor.scanInBufferRange pairRegexes[startPair], scanRange, (result) ->
+    @editor.scanInBufferRange @pairRegexes[startPair], scanRange, (result) ->
       switch result.match[0]
         when startPair
           unpairedCount++
@@ -154,7 +180,7 @@
     scanRange = new Range([0, 0], endPairPosition)
     startPairPosition = null
     unpairedCount = 0
-    @editor.backwardsScanInBufferRange pairRegexes[startPair], scanRange, (result) ->
+    @editor.backwardsScanInBufferRange @pairRegexes[startPair], scanRange, (result) ->
       switch result.match[0]
         when startPair
           unpairedCount--
@@ -167,8 +193,8 @@

   findAnyStartPair: (cursorPosition) ->
     scanRange = new Range([0, 0], cursorPosition)
-    startPair = _.escapeRegExp(_.keys(startPairMatches).join(''))
-    endPair = _.escapeRegExp(_.keys(endPairMatches).join(''))
+    startPair = _.escapeRegExp(_.keys(@startPairMatches).join(''))
+    endPair = _.escapeRegExp(_.keys(@endPairMatches).join(''))
     combinedRegExp = new RegExp("[#{startPair}#{endPair}]", 'g')
     startPairRegExp = new RegExp("[#{startPair}]", 'g')
     endPairRegExp = new RegExp("[#{endPair}]", 'g')
@@ -268,7 +294,7 @@
     else
       if startPosition = @findAnyStartPair(@editor.getCursorBufferPosition())
         startPair = @editor.getTextInRange(Range.fromPointWithDelta(startPosition, 0, 1))
-        endPosition = @findMatchingEndPair(startPosition, startPair, startPairMatches[startPair])
+        endPosition = @findMatchingEndPair(startPosition, startPair, @startPairMatches[startPair])
       else if pair = @tagFinder.findEnclosingTags()
         {startRange, endRange} = pair
         if startRange.compare(endRange) > 0


Enabled ISO installing method

Enabled ISO installing method

--- app/modules/new-vm/index.coffee
+++ app/modules/new-vm/index.coffee
@@ -40,6 +40,7 @@
     $scope.updateResourceSet = (resourceSet) ->
       $scope.templates = []
       $scope.writable_SRs = []
+      $scope.ISO_SRs = []
       srs = []
       $scope.resourceSetNetworks = []
       $scope.pools = []
@@ -52,6 +53,7 @@
         else if obj.type is 'network'
           $scope.resourceSetNetworks.push(obj)
       $scope.writable_SRs = filter(srs, (sr) => sr.content_type isnt 'iso')
+      $scope.ISO_SRs = filter(srs, (sr) => sr.content_type is 'iso')

     $scope.multipleVmsActive = false


coffee版本

coffee版本

--- lib/coffee/selectionSort.coffee
+++ lib/coffee/selectionSort.coffee
@@ -11,7 +11,6 @@
         tmp = arrToSort[i]
         arrToSort[i] = arrToSort[j]
         arrToSort[j] = tmp
-    console.log arrToSort

   console.log "选择排序：消耗了: " + (moment() - timeStart) + "ms"
   arrToSort


Add additional ozaria sound options to level editor (#136)

--- app/schemas/models/level.coffee
+++ app/schemas/models/level.coffee
@@ -147,7 +147,7 @@
     suppressSelectionSounds: {type: 'boolean', title: 'Suppress Selection Sounds', description: 'Whether to suppress selection sounds made from clicking on Thangs.'}
     music: c.object {title: 'Music', description: 'Control music playing'},
       play: {title: 'Play', type: 'boolean'}
-      file: c.shortString(title: 'File', enum: ['/music/music_level_1', '/music/music_level_2', '/music/music_level_3', '/music/music_level_4', '/music/music_level_5'])
+      file: c.shortString(title: 'File', enum: ['/music/music_level_1', '/music/music_level_2', '/music/music_level_3', '/music/music_level_4', '/music/music_level_5', '/music/OzariaLoopMusic1', '/music/OzariaLoopMusic2', '/music/OzariaLoopMusic3', '/music/OzariaLoopMusic5', '/music/SpiritLandsAmbient01'])

 ScriptSchema = c.object {
   title: 'Script'


suppress logging in ZipManager tests

suppress logging in ZipManager tests

--- services/document-updater/test/unit/coffee/ZipManager/ZipManager.coffee
+++ services/document-updater/test/unit/coffee/ZipManager/ZipManager.coffee
@@ -12,6 +12,7 @@
 		rclient = null
 		beforeEach (done) ->
 			@ZipManager = SandboxedModule.require zipModulePath,	requires:
+				"logger-sharelatex": log:->
 				'settings-sharelatex': redis:
 					web:
 						host: 'none'
@@ -60,6 +61,7 @@
 		rclient = null
 		beforeEach (done) ->
 			@ZipManager = SandboxedModule.require zipModulePath,	requires:
+				"logger-sharelatex": log:->
 				'settings-sharelatex': redis:
 					web:
 						host: 'none'
@@ -109,6 +111,7 @@
 		rclient = null
 		beforeEach (done) ->
 			@ZipManager = SandboxedModule.require zipModulePath,	requires:
+				"logger-sharelatex": log:->
 				'settings-sharelatex': redis:
 					web:
 						host: 'none'


ABORT!

ABORT!

--- ext/user_heartbeat.coffee
+++ ext/user_heartbeat.coffee
@@ -8,7 +8,8 @@
   if clientId && userId && message.channel.match(/meta\/connect/ig)
     console.log "User ID: #{userId}"
     startHeartbeat(userId,clientId)
-  else if clientId && message.channel.match(/meta\/subscribe/ig)
+
+  if clientId && message.channel.match(/meta\/subscribe/ig) && userId == undefined
     privateChannelMatch = /^\/users\/(.{24})\/private$/ig.exec(message.subscription)
     if privateChannelMatch
       userId = privateChannelMatch[1]


Add documentation.

Add documentation.

--- examples/express/lib/app.coffee
+++ examples/express/lib/app.coffee
@@ -10,12 +10,20 @@
 app.set 'view engine', 'jade'
 app.locals.pretty = true

+# Stylus middleware. Serves bunlded CSS/Stylus files from assets/css at
+# static/css.
 app.use '/static/css', stylus.middleware
   src: root '/assets/css'
   dest: root '/static/css'

+# Express static middleware. Having this first in the middleware stack ensures
+# that static js files are still served normally, instead of being bunlded by
+# requisite.
 app.use express.static root()

+# Requisite middleware. Takes an entry module and bundles all dependencies
+# required from there. Serves bundled JavaScript files from assets/js at
+# static/js.
 app.use '/static/js', requisite.middleware
   export: 'app'
   entry: root '/assets/js/app'


::getModel() returns the editor

::getModel() returns the editor

--- src/react-editor-view.coffee
+++ src/react-editor-view.coffee
@@ -170,7 +170,7 @@
     @editor.pageUp()

   getModel: ->
-    @component?.getModel()
+    @editor

   getFirstVisibleScreenRow: ->
     @editor.getVisibleRowRange()[0]


Add h-p-w modifications to use new sound engine

--- app/views/home-page-view.coffee
+++ app/views/home-page-view.coffee
@@ -33,9 +33,9 @@
     # the first level
     level = 'level1'

-    if mediator.playWithSounds
-      @subscribeEvent 'soundsLoaded', =>
-        @soundManager.startAll()
+    # if mediator.playWithSounds
+    #   @subscribeEvent 'soundsLoaded', =>
+    #     @soundManager.startAll()

     @subscribeEvent 'changeLvl', =>
       console.log 'change level'
@@ -62,6 +62,7 @@
     mediator.entities = []
     @physicsManager.setup()
     @entitySpawnManager.initialSpawn()
+    @soundManager.startAll()

     window.requestAnimationFrame @doTheWork


Fix #334: Make links without `href` markable

Commit be94b559b:

> - Only `<a>` elements with a `href` attribute should be matched, since
>   HTML5 allows `<a>` elements to omit `href`:
>
>   > If the a element has no href attribute, then the element represents
>   > a placeholder for where a link might otherwise have been placed, if
>   > it had been relevant, consisting of just the element's contents.
>
>   Reference:
>   <http://developers.whatwg.org/text-level-semantics.html#the-a-element>

That’s the spec. In the reality, people use all sorts of weird markup.
Some sites (such as StackExchange sites) leave out the `href` property
and use the anchor as a JavaScript-powered button (instead of just using
the `button` element).

This commit reverts the change to link matching in be94b559b. A side
effect is that sites using links without `href` in compliance with the
spec will get extranous markers for those links. It’s better to show too
many markers than too few, though.

--- extension/packages/utils.coffee
+++ extension/packages/utils.coffee
@@ -328,8 +328,11 @@
   seen = {}
   return array.filter((item) -> if seen[item] then false else (seen[item] = true))

+# Why isn’t `a[@href]` used, when `area[@href]` is? Some sites (such as
+# StackExchange sites) leave out the `href` property and use the anchor as a
+# JavaScript-powered button (instead of just using the `button` element).
 ACTION_ELEMENT_TAGS = [
-  "a[@href]"
+  "a"
   "area[@href]"
   "button"
 ]


Fix focus when replying to message, refs #43

--- client/app/components/compose.coffee
+++ client/app/components/compose.coffee
@@ -413,9 +413,7 @@
                 node = @refs.html?.getDOMNode()
                 if not node?
                     return
-                setTimeout ->
-                    jQuery(node).focus()
-                , 0
+                document.querySelector(".rt-editor").focus()
                 if not @props.settings.get 'composeOnTop'
                     node.innerHTML += "

"
                     node = node.lastChild
@@ -429,6 +427,7 @@
                         s.removeAllRanges()
                         s.addRange(r)
                         document.execCommand('delete', false, null)
+                        node.focus()

             # Some DOM manipulation when replying inside the message.
             # When inserting a new line, we must close all blockquotes,
@@ -445,14 +444,19 @@
                               document.documentElement.msMatchesSelector

                         target = document.getSelection().anchorNode
+                        targetElement = target
+                        while not (targetElement instanceof Element)
+                            targetElement = targetElement.parentNode
                         if not target?
                             return
-                        if matchesSelector? and not matchesSelector.call(target, '.rt-editor blockquote *')
+                        if matchesSelector? and not matchesSelector.call(targetElement, '.rt-editor blockquote *')
                             # we are not inside a blockquote, nothing to do
                             return

-                        if target.lastChild
-                            target = target.lastChild.previousElementSibling
+                        if target.lastChild?
+                            target = target.lastChild
+                            if target.previousElementSibling?
+                                target = target.previousElementSibling
                         parent = target

                         # alternative 1
@@ -535,12 +539,13 @@
                     header = jQuery('.rt-editor blockquote').eq(0).prev()
                     header.text(header.text().replace('…', ''))
                     header.append('…</>')
+                    header.on 'click', ->
+                        jQuery('.rt-editor').toggleClass('folded')
                 catch e
                     console.log e
-            jQuery('.rt-editor .originalToggle').on('click', ->
-                console.log 'click'
+            jQuery('.rt-editor .originalToggle').on 'click', ->
                 jQuery('.rt-editor').toggleClass('folded')
-            )
+
         else
             # Text message
             if @props.focus


inlcude email address in information returned by scraperwiki.user.profile()

--- client/code/view/tool/content.coffee
+++ client/code/view/tool/content.coffee
@@ -79,9 +79,11 @@
             real:
               shortName: window.user.real.shortName
               displayName: window.user.real.displayName
+              email: window.user.real.email[0]
             effective:
               shortName: window.user.effective.shortName
               displayName: window.user.effective.displayName
+              email: window.user.effective.email[0]

 class Cu.View.AppContent extends Cu.View.ToolContent


Prefer referential equality over packing into Some

--- src/Bacon.coffee
+++ src/Bacon.coffee
@@ -174,13 +174,13 @@
             reply
           else
             setValue(event.value)
-            if _.all(_.map(((x) -> x.isDefined), values))
+            if _.all(values, ((x) -> x != None))
               if initialSent and event.isInitial()
                 # don't send duplicate Initial
                 Bacon.more
               else
                 initialSent = true
-                valueArrayF = -> (x.get()() for x in values)
+                valueArrayF = -> (x() for x in values)
                 reply = sink(event.apply(valueArrayF))
                 unsubAll() if reply == Bacon.noMore
                 reply
@@ -189,7 +189,7 @@
       sinkFor = (index, stream) ->
         combiningSink(
           (-> ends[index] = true)
-          ((x) -> values[index] = new Some(x))
+          ((x) -> values[index] = x)
           stream)
       subs = for stream, index in streams
         do (stream, index) ->


Hide download button on local network

Hide download button on local network

--- extras/Studio.framer/app.coffee
+++ extras/Studio.framer/app.coffee
@@ -4,6 +4,7 @@
 	twitter: "jornvandijk"
 	title: "Android Tabs"
 	description: "Here's a new Framer example. It's a little grid of photos, which you can scroll, click and pinch to zoom. Made to highlight some of our latest features: scroll and click separation, pinchable layers, event shortcuts and more."
+	local: true

 # Setup
 context = new Framer.Context({name: "Sharing"})
@@ -38,7 +39,7 @@
 		@_renderCTA()
 		@_renderInfo()
 		@_renderDescription() if @shareInfo.description
-		@_renderDownload()
+		@_renderDownload() if !@shareInfo.local

 		# Evaluate content and set height accordingly
 		@_updateHeight()


Fix title/description functionality

Fix title/description functionality

Only get the first title/description, remove all newlines, remove all superfluous white space. In an ideal world this would not be needed but I have experienced many sites with newlines and spaces in their title tags along with multiple titles. Sometimes the title is in <body> as well. At the time of writing ello.co has 83 occurrences of </title> on its home page. See here for another great title example: http://www.carphonewarehouse.com/phones/nokia-105/

--- src/scripts/http-info.coffee
+++ src/scripts/http-info.coffee
@@ -45,8 +45,8 @@
     done = (errors, window) ->
       unless errors
         $ = window.$
-        title = $('title').text()
-        description = $('meta[name=description]').attr("content") || ""
+        title = $('title').first().text().replace(/(\r\n|\n|\r)/gm,"").replace(/\s{2,}/g,' ').trim()
+        description = $('meta[name=description]').first().attr("content").replace(/(\r\n|\n|\r)/gm,"").replace(/\s{2,}/g,' ').trim() || ""

         if title and description and not process.env.HUBOT_HTTP_INFO_IGNORE_DESC
           msg.send "#{title}\n#{description}"


improve getSurvey method to work from inside collection

--- jsapp/xlform/src/model.base.coffee
+++ jsapp/xlform/src/model.base.coffee
@@ -78,8 +78,13 @@
       parent = @_parent
       if parent == null
         return null
-      while parent._parent
-        parent = parent._parent
+      while parent._parent or parent.collection
+        if parent._parent
+          parent = parent._parent
+        else if parent.collection and parent.collection._parent
+          parent = parent.collection._parent
+        else
+          break
       parent

   _innerValue = (val)->


Added missing template arg.

Added missing template arg.

--- src/Forms.coffee
+++ src/Forms.coffee
@@ -376,7 +376,7 @@
     Form.getElement = (template) -> Forms.getFormElement(getTemplate(template))

     Form.getFieldElement = (name, template) ->
-      Forms.getFieldElement(name, Form.getElement(), template)
+      Forms.getFieldElement(name, Form.getElement(template), template)

     Form.getSchemaInputs = (template) ->
       template = getTemplate(template)


Fix QFN pattern generation

Fix QFN pattern generation

--- src/pattern/default/qfn.coffee
+++ src/pattern/default/qfn.coffee
@@ -5,7 +5,8 @@
 module.exports = (pattern, element) ->
   housing = element.housing
   leadCount = housing.leadCount ? 2*(housing.rowCount + housing.columnCount)
-  if housing.tabWidth? and housing.tabLength then ++leadCount
+  hasTab = housing.tabWidth? and housing.tabLength?
+  if hasTab then ++leadCount
   height = housing.height.max ? housing.height
   pattern.name ?= sprintf "%sQFN%dP%dX%dX%d-%d",
     if housing.pullBack? then 'P' else '',
@@ -40,6 +41,21 @@
     layer: ['topCopper', 'topMask', 'topPaste']

   quad pattern, padParams
+
+  if hasTab
+    housing.tabOffset ?= '0, 0'
+    [x, y] = housing.tabOffset.replace(/\s+/g, '').split(',').map((a) => parseFloat(a))
+
+    tabNumber = leadCount
+    tabPad =
+      type: 'smd'
+      shape: 'rectangle'
+      width: housing.tabWidth.nom
+      height: housing.tabLength.nom
+      layer: ['topCopper', 'topMask', 'topPaste']
+      x: x
+      y: y
+    pattern.pad tabNumber, tabPad

   firstPad = pattern.pads[1]
   lastPad = pattern.pads[2*(padParams.rowCount + padParams.columnCount)]
@@ -111,8 +127,9 @@

   # Courtyard
   courtyard = padParams.courtyard
-  x = bodyWidth/2 + courtyard
-  y = bodyLength/2 + courtyard
+
+  x = Math.min(-bodyWidth/2, firstPad.x - firstPad.width/2) - courtyard
+  y = Math.min(-bodyLength/2, lastPad.y - lastPad.height/2) - courtyard

   pattern
     .layer 'topCourtyard'


Accessibility: fix for focus trap

Accessibility: fix for focus trap

--- @client/form.coffee
+++ @client/form.coffee
@@ -144,7 +144,6 @@
         title: 'Directly edit HTML'
         onClick: => @local.edit_code = true; save @local

-
     DIV
       id: @props.key
       style:
@@ -169,8 +168,6 @@

       else

-
-
         DIV null,

           Dock
@@ -180,6 +177,7 @@

             # Toolbar
             DIV
+              ref: 'toolbar'
               role: 'toolbar'
               'title': 'Rich text markup'
               'aria-orientation': 'vertical'
@@ -192,11 +190,15 @@
                 top: 0
                 display: 'block'
                 visibility: if wysiwyg_editor.showing != @props.key then 'hidden'
+
               onFocus: (e) =>
-                if !@local.focused_toolbar_item?
-                  @local.focused_toolbar_item = 0
-                  save @local
-                @refs["toolbaritem-#{@local.focused_toolbar_item}"].getDOMNode().focus()
+                if !@local.focused_toolbar_item && !@local.just_unfocused
+
+                  if !@local.focused_toolbar_item?
+                    @local.focused_toolbar_item = 0
+                    save @local
+
+                  @refs["toolbaritem-#{@local.focused_toolbar_item}"].getDOMNode().focus()

               onKeyDown: (e) =>
                 if e.which in [37, 38, 39, 40]
@@ -234,9 +236,18 @@
                     title: button.title
                     value: if button.value then button.value
                     onClick: if button.onClick then button.onClick
-                    onFocus: => @local.focused_toolbar_item = idx ; save @local
-                    onBlur: =>
+                    onFocus: (e) =>
+                      @local.focused_toolbar_item = idx;
+                      save @local
+                      e.stopPropagation()
+
+                    onBlur: (e) =>
+                      e.stopPropagation()
                       @local.focused_toolbar_item = null
+                      @local.just_unfocused = true
+                      setTimeout =>
+                        @local.just_unfocused = false
+                      , 0
                       save @local

                       # if the focus isn't still on an element inside of this menu,
@@ -256,7 +267,7 @@
               id: 'editor'
               dangerouslySetInnerHTML:{__html: @props.html}
               'data-placeholder': if show_placeholder then @props.placeholder else ''
-              onFocus: =>
+              onFocus: (e) =>
                 # Show the toolbar on focus
                 # showing is global state for the toolbar to be
                 # shown. It gets set to null when someone clicks outside the
@@ -266,6 +277,7 @@
                 wysiwyg_editor = fetch 'wysiwyg_editor'
                 wysiwyg_editor.showing = @props.key
                 save wysiwyg_editor
+
               onBlur: =>
                 # if the focus isn't still on an element inside of this menu,
                 # then we should close the menu


webapp/latex: trashing aux files, sometimes no output -- fixes #1774

--- src/smc-webapp/editor.coffee
+++ src/smc-webapp/editor.coffee
@@ -2388,7 +2388,10 @@
                     command: 'latexmk'
                     args   : ['-c', @base_filename]
                     cb     : (err, output) ->
-                        log += output.stdout + '\n\n' + output.stderr + '\n\n'
+                        if output?
+                            log += output.stdout + '\n\n' + output.stderr + '\n\n'
+                        if err
+                            log += "#{err}" + '\n\n'
                         cb(err)
             (cb) =>
                 # this in particular gets rid of the sagetex files
@@ -2399,7 +2402,10 @@
                     command : "rm"
                     args    : ['-v', '-f', '--'].concat(files)
                     cb      : (err, output) ->
-                        log += output.stdout + '\n\n' + output.stderr + '\n\n'
+                        if output?
+                            log += output.stdout + '\n\n' + output.stderr + '\n\n'
+                        if err
+                            log += "#{err}" + '\n\n'
                         cb(err)
         ], (err) =>
             log += 'done.'


webapp/chat: simple fix for #1802

webapp/chat: simple fix for #1802

--- src/smc-webapp/smc_chat.cjsx
+++ src/smc-webapp/smc_chat.cjsx
@@ -556,6 +556,7 @@
                 s: value
                 wrap: ['', '']
             value = misc_page.sanitize_html(value)
+            file_path = if @props.path? then misc.path_split(@props.path).head

             <Row ref="preview" style={position:'absolute', bottom:'0px', width:'100%'}>
                 <Col xs={0} sm={2}></Col>
@@ -565,9 +566,7 @@

                             <Icon name='times'/>

-

-                            <Markdown value={value}/>
-

+                        {render_markdown(value, @props.project_id, file_path)}

                             Preview (press Shift+Enter to send)


string_extensions

--- src/string_extensions.coffee
+++ src/string_extensions.coffee
@@ -40,6 +40,15 @@

 ###*
 Returns a new string with the first letter capitalized and the rest lower cased.
+
+

+"capital".capitalize()
+"cAPITAL".capitalize()
+"cApItAl".capitalize()
+"CAPITAL".capitalize()
+
+# => "Capital"
+

 @name capitalize
 @methodOf String#


Fix a critical bug

Fix a critical bug

--- src/alias.coffee
+++ src/alias.coffee
@@ -16,10 +16,11 @@
   robot.receive = (msg)->
     table = robot.brain.get(ALIAS_TABLE_KEY) || {}
     orgText = msg.text?.trim()
-    if /hubot(\s)([^\s]*)/.test orgText
+    if /hubot(\s)([^\s]*)(.*)$/.test orgText
       sp = RegExp.$1
       action = RegExp.$2
-      msg.text = "hubot#{sp}#{table[action] || action}" if action != 'alias'
+      rest = RegExp.$3
+      msg.text = "hubot#{sp}#{table[action] || action}#{rest}" if action != 'alias'
     console.log "Change \"#{orgText}\" as \"#{msg.text}\"" if orgText != msg.text

     receiveOrg.bind(robot)(msg)


Fixed bug in collection_view.coffee

Fixed bug in collection_view.coffee

In function `endAnimation` changed `elem.opacity = 1` to `elem.style.opacity = 1`. Came across this bug when using Exoskeleton without jquery.

--- src/chaplin/views/collection_view.coffee
+++ src/chaplin/views/collection_view.coffee
@@ -46,7 +46,7 @@
   else
     (elem, duration) ->
       elem.style.transition = "opacity #{(duration / 1000)}s"
-      elem.opacity = 1
+      elem.style.opacity = 1

 insertView = do ->
   if $


More conversion to Coffee Script

More conversion to Coffee Script

--- src/basic.coffee
+++ src/basic.coffee
@@ -100,23 +100,21 @@

       // xhr string string -> void
-      function errorWrapper(xhr, text, error) {
+      errorWrapper = (xhr, text, error) ->
         @log(LogLevel.DEBUG, "myna.suggest errorWrapper called")

-        var response = parseErrorResponse(xhr.responseText)
+        response = parseErrorResponse(xhr.responseText)
         @log(LogLevel.ERROR, xhr)
         @log(LogLevel.ERROR, text)
         @log(LogLevel.ERROR, error)
         @log(LogLevel.ERROR, response)
         @log(LogLevel.ERROR, "myna.suggest failed: error " + response.code + " " + response.message)

-        if(error) {
+        if error
           error(response.code, response.message)
-        }
-      }

     myna.doAjax("/suggest", data, successWrapper, errorWrapper)
-  }
+

   /** @type { function(number, function(), ?function(number, string)) } */
   myna.reward = function(amount, success, error) {


Fix cleaning of multiple build paths

Fix cleaning of multiple build paths

--- src/beans.coffee
+++ src/beans.coffee
@@ -255,11 +255,11 @@
   target ?= 'all'
   return unless knownTarget 'clean', target, ['build', 'docs', 'all']
   info = loadInfo()
-  targets = (target for target of info.paths)
+  paths = (pth for _, pth of info.paths)
   switch target
-    when 'build' then rmrf dir for dir in targets.concat('build')
+    when 'build' then rmrf dir for dir in paths.concat('build')
     when 'docs' then rmrf 'docs'
-    when 'all' then rmrf dir for dir in targets.concat('build', 'docs')
+    when 'all' then rmrf dir for dir in paths.concat('build', 'docs')
   return

 # Generate documentation files using Docco.


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -274,13 +274,19 @@
     ###*
     Returns a copy of the calling color darkened by `amount` (Lightness of the color ranges from 0 to 1).

-

+

     green = Color(0, 255, 0)

     darkGreen = green.darken(0.3)

-    darkGreen.toString()
-    # => 'rgba(0, 102, 0, 1)'
+    # to see what they look like
+    for color, index in ['green', 'darkGreen']
+      canvas.drawRect
+        color: eval(color)
+        x: 20 + (60 * index)
+        y: 20 + (60 * index)
+        width: 60
+        height: 60

     @name darken


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -41,8 +41,8 @@
     parsedColor = null

     if arguments.length == 2
-      if color.length == 3
-        parsedColor = [parseInt(color[0]), parseInt(color[1]), parseInt(color[2]), parseFloat(arguments[1])]
+      if argments[0].length == 3
+        parsedColor = [parseInt(argments[0]), parseInt(argments[1]), parseInt(argments[2]), parseFloat(arguments[1])]
     if arguments.length > 2
       alpha = if arguments[3]? then arguments[3] else 1
       parsedColor = [parseInt(arguments[0]), parseInt(arguments[1]), parseInt(arguments[2]), parseFloat(alpha)]


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -22,7 +22,7 @@
     parsedColor = null

     if color[0] != "#"
-      parsedColor = "#" + (parseColor(color) || "FFFFFF")
+      parsedColor = "#" + (arrayToHex(parseColor(color)) || "FFFFFF")
     else
       parsedColor = parseColor(color)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -18,13 +18,15 @@
           parseInt(hexString.substr(2, 1), 16) * 0x11
           if hexString.substr(3, 1).length then (parseInt(hexString.substr(3, 1), 16) * 0x11) / 255.0
         ]
-      else
+      when 6, 8
         return [
           parseInt(hexString.substr(0, 2), 16)
           parseInt(hexString.substr(2, 2), 16)
           parseInt(hexString.substr(4, 2), 16)
           if hexString.substr(6, 2).length then parseInt(hexString.substr(6, 2), 16) / 255.0 else 1.0
         ]
+      else
+        undefined

   parseRGB = (colorString) ->
     return unless bits = rgbParser.exec(colorString)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/color.coffee
+++ src/color.coffee
@@ -346,6 +346,15 @@
       else
         return hsl[2]

+    ###*
+    A copy of the calling color with its hue shifted by `degrees`. This differs from the hue setter in that it will wrap around 0 and 360.
+
+    @name shiftHue
+    @methodOf Color#
+    @param {Number} degrees number of degrees to shift the hue on the color wheel.
+
+    @returns {Color} returns the color object if you pass a new lightness value and returns the lightness otherwise
+    ###
     shiftHue: (degrees) ->
       @copy().shiftHue$(degrees)


cleaning up array constructions

cleaning up array constructions

--- src/color.coffee
+++ src/color.coffee
@@ -80,10 +80,13 @@
     else if args.length == 1 && Object.isArray(args.first())
       arr = args.first()

-      alpha = if arr[3]? then arr[3] else 1
-
-      parsedColor = arr.map (channel) ->
-        parseFloat(channel)
+      alpha = if arr[3]? then parseFloat arr[3] else 1.0
+
+      rgbMap = arr.map (channel) ->
+        parseFloat channel
+
+      parsedColor = rgbMap.concat(alpha)
+
     else if arguments.length == 2
       c = arguments[0]
       a = arguments[1]


* add all stat fileds when merge user * sort by commits temporarily

--- src/index.coffee
+++ src/index.coffee
@@ -25,6 +25,8 @@
       else
         result[user.email] = id
   result
+
+statFields = ['commits', 'percent', 'deletions', 'insertions']
 deduplicateUser = (users, emails)->
   # merge the user of the same email address
   t = users
@@ -32,8 +34,8 @@
   t.forEach (user, index)->
     for u in users
       if user.email is u.email
-        u.percent += user.percent
-        u.commits += user.commits
+        for k in statFields
+          u[k] += user[k] if u[k]?
         return
     vUserId = emails[user.email]
     user.id = vUserId if vUserId
@@ -45,8 +47,8 @@
   t.forEach (user, index)->
     for u in users
       if user.id and user.id is u.id
-        u.percent += user.percent
-        u.commits += user.commits
+        for k in statFields
+          u[k] += user[k] if u[k]?
         return
     users.push(user)
   users
@@ -93,7 +95,7 @@
         else
           u = usersCache[id]
         for k,v of user
-          if v? and not (k in ['id', 'commits', 'percent'])
+          if v? and not (k in ['id', 'commits', 'percent', 'deletions', 'insertions'])
             if k is 'email' and _.isArray(u[k])
               u[k].push v unless v in u[k]
               continue
@@ -107,7 +109,7 @@
       users
   .then (users)-> # sort by percent
     _.sortBy users, (committer)->
-      return -committer.percent
+      return -committer.commits
   .then (users)-> # filter fields:
     if _.isArray(fields)
       users.forEach (user)->


bump up sw timeout, don't error if not ready

--- src/index.coffee
+++ src/index.coffee
@@ -4,7 +4,7 @@
 RPCClient = require './rpc_client'

 DEFAULT_HANDSHAKE_TIMEOUT_MS = 10000 # 10 seconds
-SW_CONNECT_TIMEOUT_MS = 3000 # 3s
+SW_CONNECT_TIMEOUT_MS = 5000 # 5s

 selfWindow = if window? then window else self

@@ -48,7 +48,9 @@
         readyTimeout = setTimeout resolve, SW_CONNECT_TIMEOUT_MS

         navigator.serviceWorker.ready
-        .catch -> null
+        .catch ->
+          console.log 'caught sw error'
+          null
         .then (registration) =>
           worker = registration?.active
           if worker
@@ -148,7 +150,7 @@
     .catch -> null

     @swValidation = @ready.then =>
-      @sw.call 'ping', null, {timeout: @handshakeTimeout}
+      @sw?.call 'ping', null, {timeout: @handshakeTimeout}
     .then (registeredMethods) =>
       @parentsRegisteredMethods = @parentsRegisteredMethods.concat(
         registeredMethods


Expose lr on exported object. Related to #98

--- src/index.coffee
+++ src/index.coffee
@@ -127,4 +127,5 @@
         lr.changed body:
           files: file.path
       callback null, file
+  lr: lr
   serverClose: -> do server.close


Return low in save, load and on function

For easy chain, and simplify the `require` statement. Resolve #13 .

--- src/index.coffee
+++ src/index.coffee
@@ -93,14 +93,17 @@

 low.save = (path = low.path) ->
   underdb.save low.db, path
+  low

 low.throttledSave = _.throttle low.save, 100

 low.load = (path = low.path) ->
   low.db = underdb.load path
+  low

 low.on = (event, listener) ->
   ee.on event, listener
+  low

 #
 # Expose _
@@ -137,4 +140,4 @@
 # Export
 #

-module.exports = low+module.exports = low


Backtick to preserve empty stuff

Backtick to preserve empty stuff

--- src/model.coffee
+++ src/model.coffee
@@ -549,6 +549,9 @@
   exports.SocketStartToken = class SocketStartToken extends StartToken
     constructor: (@container) -> super; @type = 'socketStart'
     serialize: -> "<socket precedence=\"#{@container.precedence}\">"
+    stringify: ->
+      if @next is @container.end or
+        @next.type is 'text' and @next.value is '' then '``' else ''

   exports.SocketEndToken = class SocketEndToken extends EndToken
     constructor: (@container) -> super; @type = 'socketEnd'
@@ -575,7 +578,9 @@

   exports.IndentEndToken = class IndentEndToken extends EndToken
     constructor: (@container) -> super; @type = 'indentEnd'
-    stringify: (state) -> state.indent = state.indent[...-@container.depth]; ''
+    stringify: (state) ->
+      state.indent = state.indent[...-@container.depth]
+      if @previousVisibleToken().previousVisibleToken() is @container.start then '``' else ''
     serialize: -> "</indent>"

   exports.Indent = class Indent extends Container


More debugging utils

More debugging utils

--- src/utils.coffee
+++ src/utils.coffee
@@ -1,51 +1,43 @@
 Kona.Utils =
-  inspect: (obj, label) ->
-    # Log the properties and values of an object for debugging
-    # Optional label describes object dump
-    # Also works on arrays and strings, with unsurprising results.
-    #
-    # Usage:
-    #   var car = { color: 'red', drive: function() {} }
-    #
-    #   Kona.Utils.inspect(car)  ---->
-    #     Dumping object:
-    #       color: red
-    #       drive(): <function>
-    #
-    #   Kona.Utils.inspect(car, 'car info')  ---->
-    #     Dumping car info:
-    #       color: red
-    #       drive(): <function>

-    defined_str   = (if obj? then '' else '<undefined>')
-    spacer        = "  " # Indent results
-    is_array      = Array.isArray(obj)
-    is_string     = typeof obj == 'string'
-    is_number     = typeof obj == 'number'
+  # Debug util - inspect an object or raise an error if it's undefined
+  require: (obj) ->
+    if obj? then Kona.debug(obj) else throw new Error("Obj not defined")

-    if label?
-      Kona.debug "Dumping #{label}: #{defined_str}"
-    else
-      Kona.debug "Dumping #{ if is_array then 'array' else typeof obj}: #{defined_str}"
+  # Print a 2d array in a grid format
+  # ex: printGrid [[1,2], [3,4]]
+  #   [
+  #     [1, 2]
+  #     [3, 4]
+  #   ]
+  printGrid: (grid) ->
+    output = "[\n"

-    if is_array
-      Kona.debug "#{spacer}[#{obj}]"    # [1,2,3]
-    else if is_string
-      Kona.debug "#{spacer}\"#{obj}\""  # "test"
-    else if is_number
-      Kona.debug "#{spacer}#{obj}"      # 5
-    else
-      for own key, value of obj         # x: 25
-        if value instanceof Function
-          Kona.debug "#{spacer}#{key}(): <function>"
-        else
-          Kona.debug "#{spacer}#{key}: #{value}"
+    for row in grid
+      output += "  ["
+      for item, idx in row
+        output += "#{item}"
+        output += ", " unless idx == row.length-1
+      output += "]\n"

+    console.log "#{output}]"

+  colorFor: (num) ->
+    switch num
+      when 1 then 'red'
+      when 2 then 'orange'
+      when 3 then 'blue'
+
+  # Generate a random integer within range (inclusive)
   randomFromTo: (from, to) ->
-    # Generate a random integer within range (inclusive)
     Math.floor(Math.random() * (to - from + 1) + from)

+  # Find an object in a list of objects based on the value of one of its keys
+  # Returns the first matching object
+  # Ex:
+  #   cars = [ {color: 'red', owner: 'Jon'}, {color: 'green', owner: 'Jane'} ]
+  #   Kona.Utils.findByKey(cars, 'color', 'red')
+  #   => { color: 'red', owner: 'Jon' }
   findByKey: (list, key, value) ->
     _.find list, (item) -> item[key] == value


Set logging level to warn

Set logging level to warn

--- src/utils.coffee
+++ src/utils.coffee
@@ -100,7 +100,7 @@
 exports.WARN = 3
 exports.ERROR = 4

-_logging_level = exports.INFO
+_logging_level = exports.WARN

 exports.config = (level) ->
   _logging_level = level


Add else clause

Add else clause

--- src/world.coffee
+++ src/world.coffee
@@ -51,11 +51,14 @@
     return @ if @get_local(key)?
     @up.owner(key)

-  send: (receiver , message) ->
+  send: (receiver, message) ->
     receiver = @find_path(receiver) if _.isString(receiver)
     key = message['key']
-    receiver.call(key, message)
-    true
+    if receiver.get_raw(key)?
+      receiver.call(key, message)
+      true
+    else
+      assert false, "No event handler"

   update: (key, delta, max) ->
     result = @get(key) + delta


Use `real_root` to locate modules to be included @m9dfukc #34

--- src/zappa.coffee
+++ src/zappa.coffee
@@ -126,7 +126,7 @@
   app.set 'view engine', 'coffee'
   app.engine 'coffee', coffeecup_adapter

-  # Sets default view dir to @root (`path.dirname(module.parent.filename)`).
+  # Sets default view dir to @root
   app.set 'views', path.join(context.root, '/views')

   for verb in ['get', 'post', 'put', 'del']
@@ -276,7 +276,7 @@
       v.apply(context, [context])

   context.include = (p) ->
-    sub = if typeof p is 'string' then require path.join(context.root, p) else p
+    sub = if typeof p is 'string' then require path.join(context.real_root, p) else p
     sub.include.apply(context, [context])

   apply_helpers = (ctx) ->


Add highlighting to `utf-8-bom` keywords

--- grammars/editorconfig.cson
+++ grammars/editorconfig.cson
@@ -1,4 +1,4 @@
-name: "editorconfig"
+name: "EditorConfig"
 scopeName: "source.editorconfig"
 fileTypes: ["editorconfig"]
 patterns: [include: "#main"]
@@ -62,7 +62,7 @@
 		},{
 			# Character sets
 			name:  "constant.language.charset.encoding.${1:/downcase}.editorconfig"
-			match: "(?i)\\b(latin1|utf-8|utf-16[bl]e)\\b"
+			match: "(?i)\\b(latin1|utf-8(?:-bom)?|utf-16[bl]e)\\b"
 		},{
 			# Anything else
 			name:  "constant.language.${1:/downcase}.editorconfig"


misc/atom: parse string types.

misc/atom: parse string types.

--- misc/atom/grammars/factor.cson
+++ misc/atom/grammars/factor.cson
@@ -164,8 +164,8 @@
     name: "constant.numeric.hex.factor"
   }
   {
-    begin: "\""
-    end: "\""
+    begin: "<[[^"]*\""
+    end: "\">"
     name: "string.quoted.double.factor"
     patterns: [
       {
@@ -216,7 +216,7 @@
   escaped_characters:
     patterns: [
       {
-        match: "\\\\(\\\\|[abefnrtsv0\"]|x[a-fA-F0-9]{2}|u[a-fA-F0-9]{6})"
+        match: "\\\\(\\\\|[abefnrtsv0\"]|x[a-fA-F0-9]{2}|u[a-fA-F0-9]{6}|u{[^}]+})"
         name: "constant.character.escape.factor"
       }
       {


whoops

whoops

--- src/ui/views/trayicon.coffee
+++ src/ui/views/trayicon.coffee
@@ -12,6 +12,8 @@
 # TODO: this is all WIP
 quit = ->

+compact = (array) -> item for item in array when item
+
 create = () ->
     tray = new Tray trayIcons["read"]
     tray.setToolTip 'YakYak - Hangouts client'
@@ -24,7 +26,7 @@

 update = (unreadCount, viewstate) ->
     # update menu
-    templateContextMenu = _.compact([
+    templateContextMenu = compact([
         {
           label: 'Toggle minimize to tray'
           click: -> action 'togglewindow'


sagews:  get evals during worksheet restart to work

--- salvus/smc-webapp/sagews.coffee
+++ salvus/smc-webapp/sagews.coffee
@@ -197,9 +197,12 @@
             return false
         kill_button = buttons.find("a[href=#kill]").click () =>
             kill_button.find("i").addClass('fa-spin')
+            @_restarting = true
             @kill
                 restart : true
                 cb      : (err) =>
+                    delete @_restarting  # must happen *before* emiting the restarted event
+                    @emit('restarted', err)
                     kill_button.find("i").removeClass('fa-spin')
                     if err
                         alert_message(type:"error", message:"Unable to restart worksheet (the system might be heavily loaded causing Sage to take a while to restart -- try again in a minute)")
@@ -1593,6 +1596,13 @@

         #dbg = (m...) -> console.log("execute_cell_server_side:", m...)
         #dbg("block=#{misc.to_json(opts.block)}")
+        if @_restarting
+            @once 'restarted', (err) =>
+                if not err
+                    @execute_cell_server_side(opts)
+                else
+                    opts.cb?(err)
+            return

         cell  = opts.cell
         input = cell.input()


minor style fixes

minor style fixes

--- vendor/assets/javascripts/wice_grid_init.js.coffee
+++ vendor/assets/javascripts/wice_grid_init.js.coffee
@@ -63,15 +63,15 @@

 setupDatepicker = ->
   # check if a javascript datepicker is set
-  which_datepicker = null
+  whichDatepicker = null
   if $('.wice-grid-container .date-filter input:hidden, .wg-detached-filter .date-filter .date-filter input:hidden').length != 0
-    which_datepicker = 'js'
+    whichDatepicker = 'js'
   else if $('.wice-grid-container input:text[data-provide=datepicker], .wg-detached-filter input:text[data-provide=datepicker]').length != 0
-    which_datepicker = 'bs'
-
-  if which_datepicker == 'js'
+    whichDatepicker = 'bs'
+
+  if whichDatepicker == 'js'
     setupJsDatepicker()
-  else if which_datepicker == 'bs'
+  else if whichDatepicker == 'bs'
     setupBsDatepicker()

@@ -85,8 +85,8 @@
     return

   $('.wice-grid-container .date-filter div[id$=_date_placeholder] input:text[data-provide=datepicker]').each (index, removeLink) ->
-    $(removeLink).datepicker().on 'hide', (hello) ->
-      $self = $(hello.currentTarget)
+    $(removeLink).datepicker().on 'hide', (event) ->
+      $self = $(event.currentTarget)

       eventToTriggerOnChange = $self.data('close-calendar-event-name')


Improve highlighting of processed Tbl source

--- grammars/roff.cson
+++ grammars/roff.cson
@@ -730,6 +730,14 @@
 		{match: "(\\\\)a",   name: "constant.character.escape.leader-char.roff", captures: 1: name: "punctuation.definition.escape.roff"}
 		{match: "(\\\\)\\.", name: "constant.character.escape.dot.roff",         captures: 1: name: "punctuation.definition.escape.roff"}, {

+			# \*[name].macro - Adhoc matching to improve tbl's "\*[3trans]" hack (GNU)
+			begin: "^(\\\\\\*\\[[^\\]]+\\])\\s*(\\.\\w+.*)"
+			end: "$"
+			beginCaptures:
+				1: patterns: [include: "#escapes"]
+				2: patterns: [include: "#conditional-innards"]
+		},{
+
 			# \*[xx args…] - Interpolate string "xx" with optional arguments (GNU)
 			name: "constant.character.escape.function.interpolate-string.gnu.roff"
 			begin: "((\\\\)\\*(\\[))([^\\]\\s]+)"
@@ -1679,7 +1687,6 @@
 			},{include: "$self"}]
 		},{

-		},{
 			# .B/.SB: Bold
 			begin: "^([.'])[ \t]*(S?B)(\\s*\\\\[#\"].*$)?(?=$|[ \t]+|\\\\)"
 			end:   "^(?=[.'])|(?=\\\\\")|(?!\\\\#)((\\S+[ \t]*)(?<![^\\\\]\\\\)\n)"
@@ -2332,6 +2339,12 @@
 				1: patterns: [include: "#params"]
 				2: name: "punctuation.terminator.section.tbl.roff"
 			patterns: [{
+
+				# Not preprocessor source; abandon ship
+				begin: "^(?=\\.)"
+				end: "^(?=[.'][ \t]*TE\\b)"
+				patterns: [include: "$self"]},{
+

 				# Global options
 				match: "^(.+)(;)$"


Fix incorrect group references in pattern names

These pointed to a command line's control character instead of its name.

--- grammars/roff.cson
+++ grammars/roff.cson
@@ -79,7 +79,7 @@
 		patterns:[{

 			# Generic requests without formatting
-			name: "meta.function.request.$1.roff"
+			name: "meta.function.request.$2.roff"
 			begin: "(?x) ^([.'])[ \t]*
 				(ab|ad|af|as|bd|bp|br|c2|cc|ce|cf|ch|cs|cu|da|di|ds|dt|ec
 				|em|eo|ev|ex|fc|fi|fl|fp|ft|hc|hw|hy|ig|in|it|lc|lg|lf|ll
@@ -95,7 +95,7 @@
 		},{

 			# Flow control/conditional requests
-			name: "meta.function.request.$1.roff"
+			name: "meta.function.request.$2.roff"
 			begin: "^([.'])[ \t]*(ie|if|el)"
 			end: "(?<!\\\\)(?=\n)"
 			beginCaptures:
@@ -106,7 +106,7 @@
 		},{

 			# Macro definition: No terminator specified
-			name: "meta.function.definition.request.$1.roff"
+			name: "meta.function.definition.request.$2.roff"
 			begin: "^([.'])[ \t]*(de|am)\\s*(\\S+?)?\\s*(\\\\\".*)?$"
 			end:   "^\\.[ \t]*\\."
 			beginCaptures:
@@ -121,7 +121,7 @@
 		},{

 			# Macro definition: Terminator included
-			name: "meta.function.definition.request.with-terminator.$1.roff"
+			name: "meta.function.definition.request.with-terminator.$2.roff"
 			begin: "^([.'])[ \t]*(de|am)\\s*(\\S+)\\s*(\"[^\"]+\"?|\\S+?(?=\\s|\\\\\"))?(.*)$"
 			end:   "^(\\.)[ \t]*((\\4)(?=$|\\s|\\\\(?:$|\")))"
 			beginCaptures:


Fix langauge.

Fix langauge.

--- grammars/sifu.cson
+++ grammars/sifu.cson
@@ -541,6 +541,9 @@
             'include': '#modelDtoSetting'
           }
           {
+            'include': '#visible'
+          }
+          {
             'match': '[^(\\s\\{\\}\\[\\]\\(\\)#,)]+'
             'name': 'invalid.illegal'
           }
@@ -640,6 +643,9 @@
         'patterns': [
           {
             'include': '#comments'
+          }
+          {
+            'include': '#onLoad'
           }
           {
             'include': '#onInit'
@@ -7603,6 +7609,9 @@
             'include': '#modelDtoSetting'
           }
           {
+            'include': '#visible'
+          }
+          {
             'include': '#secured'
           }
           {
@@ -7708,6 +7717,12 @@
         'patterns': [
           {
             'include': '#comments'
+          }
+          {
+            'include': '#onLoad'
+          }
+          {
+            'include': '#onInit'
           }
           {
             'include': '#onEvent'
@@ -11362,6 +11377,15 @@
             'include': '#secured'
           }
           {
+            'include': '#loadEndpointSetting'
+          }
+          {
+            'include': '#modelDtoSetting'
+          }
+          {
+            'include': '#visible'
+          }
+          {
             'match': '[^(\\s\\{\\}\\[\\]\\(\\)#,)]+'
             'name': 'invalid.illegal'
           }


Fix typo

Fix typo

--- views/components/settings/parts/plugin-config.cjsx
+++ views/components/settings/parts/plugin-config.cjsx
@@ -246,7 +246,7 @@
                             id="mirror-select">
               {
                 for server, index in mirror
-                  <MenuItem key={index} onSelect={@onSelectServer.bind @, index}>mirror[index].name</MenuItem>
+                  <MenuItem key={index} onSelect={@onSelectServer.bind @, index}>{mirror[index].name}</MenuItem>
               }
             </DropdownButton>
           </ButtonGroup>


Add special handling for highlight declarations

Fixes inconsistent highlighting in many Vimscript language files.

--- grammars/viml.cson
+++ grammars/viml.cson
@@ -49,6 +49,7 @@
 			{include: "#hashbang"}
 			{include: "#numbers"}
 			{include: "#syntax"}
+			{include: "#highlightLink"}
 			{include: "#funcDef"}
 			{include: "#augGroup"}
 			{include: "#parameter"}
@@ -282,6 +283,27 @@
 	supportVariable:
 		name: "support.variable.viml"
 		match: "\\b(?:am(?:enu)?|(?:hl|inc)?search|[Bb]uf(?:[Nn]ew[Ff]ile|[Rr]ead)?|[Ff]ile[Tt]ype)\\b"
+
+
+	# Highlighting link, used heavily in syntax-definition files
+	highlightLink:
+		match: "(?x)^\\s*
+			(:)? \\s*              (?# 1: punctuation.separator.key-value.colon.viml)
+			(hi|highlight)         (?# 2: support.function.highlight.viml)
+			(!)?                   (?# 3: storage.modifier.force.viml)
+			(?:\\s+(def|default))? (?# 4: support.function.highlight-default.viml)
+			(?:\\s+(link))         (?# 5: support.function.highlight-link.viml)
+			(?:\\s+([-\\w]+))      (?# 6: variable.parameter.group-name.viml)
+			(?:\\s+(?:(NONE)|([-\\w]+)))?"
+		captures:
+			1: name: "punctuation.separator.key-value.colon.viml"
+			2: name: "support.function.highlight.viml"
+			3: name: "storage.modifier.force.viml"
+			4: name: "support.function.highlight-default.viml"
+			5: name: "support.function.highlight-link.viml"
+			6: name: "variable.parameter.group-name.viml"
+			7: name: "support.constant.highlighting.viml"
+			8: name: "variable.parameter.group-name.viml"

 	# Syntax highlighting definitions


Remove orphan code

Remove orphan code

--- views/components/settings/parts/plugin-config.cjsx
+++ views/components/settings/parts/plugin-config.cjsx
@@ -260,18 +260,6 @@
   checkCount: 0
   emitReload: ->
     PluginManager.emitReload()
-  readPlugins: async ->
-    initState = @getInitialState()
-    initState.reloading = true
-    @setState initState
-    mirrors = yield PluginManager.getMirrors()
-    config = yield PluginManager.getConf()
-    yield PluginManager.readPlugins()
-    @updateFromPluginManager {
-      mirrors: mirrors
-      config: config
-      reloading: false
-    }
   updateFromPluginManager: async (newState) ->
     newState ?= {}
     state = @state


Use sound-only notify

Use sound-only notify

--- index.cjsx
+++ index.cjsx
@@ -206,7 +206,7 @@
     nowHourString = nowHour.toString()
     if nowHourString.length < pad.length
       nowHourString = (pad + nowHourString).slice(-pad.length)
-    notify __("It's %s now", "#{nowHourString}00"),
+    notify null,
       title: __ 'Hourly Voice'
       audio: "http://#{server}/kcs/sound/kc#{shipFilename}/#{audioFN}.mp3"


except mission result

except mission result

--- index.cjsx
+++ index.cjsx
@@ -288,7 +288,7 @@
       isPractice = path.match /practice/
       isCombined = path.match /combined/
       isWater = path.match /water/
-      if isResult?
+      if isResult? && (isBattle? || isPractice)
         if isPractice == null
           tmpShip = " "
           for i in [0..5]