Update RangeTracker to support upserting and moving comments for cut and paste

--- services/document-updater/app/coffee/RangesTracker.coffee
+++ services/document-updater/app/coffee/RangesTracker.coffee
@@ -1,5 +1,8 @@
-load = (EventEmitter) ->
-	class RangesTracker extends EventEmitter
+# This file is shared between document-updater and web, so that the server and client share
+# an identical track changes implementation. Do not edit it directly in web or document-updater,
+# instead edit it at https://github.com/sharelatex/ranges-tracker, where it has a suite of tests
+load = () ->
+	class RangesTracker
 		# The purpose of this class is to track a set of inserts and deletes to a document, like
 		# track changes in Word. We store these as a set of ShareJs style ranges:
 		#   {i: "foo", p: 42} # Insert 'foo' at offset 42
@@ -36,6 +39,7 @@
 		#     middle of a previous insert by the first user, the original insert will be split into two.
 		constructor: (@changes = [], @comments = []) ->
 			@setIdSeed(RangesTracker.generateIdSeed())
+			@resetDirtyState()

 		getIdSeed: () ->
 			return @id_seed
@@ -75,8 +79,15 @@
 			comment = @getComment(comment_id)
 			return if !comment?
 			@comments = @comments.filter (c) -> c.id != comment_id
-			@emit "comment:removed", comment
-
+			@_markAsDirty comment, "comment", "removed"
+
+		moveCommentId: (comment_id, position, text) ->
+			for comment in @comments
+				if comment.id == comment_id
+					comment.op.p = position
+					comment.op.c = text
+					@_markAsDirty comment, "comment", "moved"
+
 		getChange: (change_id) ->
 			change = null
 			for c in @changes
@@ -89,6 +100,18 @@
 			change = @getChange(change_id)
 			return if !change?
 			@_removeChange(change)
+
+		validate: (text) ->
+			for change in @changes
+				if change.op.i?
+					content = text.slice(change.op.p, change.op.p + change.op.i.length)
+					if content != change.op.i
+						throw new Error("Change (#{JSON.stringify(change)}) doesn't match text (#{JSON.stringify(content)})")
+			for comment in @comments
+				content = text.slice(comment.op.p, comment.op.p + comment.op.c.length)
+				if content != comment.op.c
+					throw new Error("Comment (#{JSON.stringify(comment)}) doesn't match text (#{JSON.stringify(content)})")
+			return true

 		applyOp: (op, metadata = {}) ->
 			metadata.ts ?= new Date()
@@ -103,29 +126,37 @@
 				@addComment(op, metadata)
 			else
 				throw new Error("unknown op type")
-
+
+		applyOps: (ops, metadata = {}) ->
+			for op in ops
+				@applyOp(op, metadata)
+
 		addComment: (op, metadata) ->
-			# TODO: Don't allow overlapping comments?
-			@comments.push comment = {
-				id: op.t or @newId()
-				op: # Copy because we'll modify in place
-					c: op.c
-					p: op.p
-					t: op.t
-				metadata
-			}
-			@emit "comment:added", comment
-			return comment
+			existing = @getComment(op.t)
+			if existing?
+				@moveCommentId(op.t, op.p, op.c)
+				return existing
+			else
+				@comments.push comment = {
+					id: op.t or @newId()
+					op: # Copy because we'll modify in place
+						c: op.c
+						p: op.p
+						t: op.t
+					metadata
+				}
+				@_markAsDirty comment, "comment", "added"
+				return comment

 		applyInsertToComments: (op) ->
 			for comment in @comments
 				if op.p <= comment.op.p
 					comment.op.p += op.i.length
-					@emit "comment:moved", comment
+					@_markAsDirty comment, "comment", "moved"
 				else if op.p < comment.op.p + comment.op.c.length
 					offset = op.p - comment.op.p
 					comment.op.c = comment.op.c[0..(offset-1)] + op.i + comment.op.c[offset...]
-					@emit "comment:moved", comment
+					@_markAsDirty comment, "comment", "moved"

 		applyDeleteToComments: (op) ->
 			op_start = op.p
@@ -138,7 +169,7 @@
 				if op_end <= comment_start
 					# delete is fully before comment
 					comment.op.p -= op_length
-					@emit "comment:moved", comment
+					@_markAsDirty comment, "comment", "moved"
 				else if op_start >= comment_end
 					# delete is fully after comment, nothing to do
 				else
@@ -161,12 +192,13 @@

 					comment.op.p = Math.min(comment_start, op_start)
 					comment.op.c = remaining_before + remaining_after
-					@emit "comment:moved", comment
+					@_markAsDirty comment, "comment", "moved"

 		applyInsertToChanges: (op, metadata) ->
 			op_start = op.p
 			op_length = op.i.length
 			op_end = op.p + op_length
+			undoing = !!op.u

 			already_merged = false
@@ -184,8 +216,9 @@
 						change.op.p += op_length
 						moved_changes.push change
 					else if op_start == change_start
-						# If the insert matches the start of the delete, just remove it from the delete instead
-						if change.op.d.length >= op.i.length and change.op.d.slice(0, op.i.length) == op.i
+						# If we are undoing, then we want to cancel any existing delete ranges if we can.
+						# Check if the insert matches the start of the delete, and just remove it from the delete instead if so.
+						if undoing and change.op.d.length >= op.i.length and change.op.d.slice(0, op.i.length) == op.i
 							change.op.d = change.op.d.slice(op.i.length)
 							change.op.p += op.i.length
 							if change.op.d == ""
@@ -203,15 +236,15 @@
 					# Only merge inserts if they are from the same user
 					is_same_user = metadata.user_id == change.metadata.user_id

-					# If this is an insert op at the end of an existing insert with a delete following, and it cancels out the following
-					# delete then we shouldn't append it to this insert, but instead only cancel the following delete.
+					# If we are undoing, then our changes will be removed from any delete ops just after. In that case, if there is also
+					# an insert op just before, then we shouldn't append it to this insert, but instead only cancel the following delete.
 					# E.g.
 					#                   foo|<--- about to insert 'b' here
 					#  inserted 'foo'  --^ ^-- deleted 'bar'
 					# should become just 'foo' not 'foob' (with the delete marker becoming just 'ar'), .
 					next_change = @changes[i+1]
 					is_op_adjacent_to_next_delete = next_change? and next_change.op.d? and op.p == change_end and next_change.op.p == op.p
-					will_op_cancel_next_delete = is_op_adjacent_to_next_delete and next_change.op.d.slice(0, op.i.length) == op.i
+					will_op_cancel_next_delete = undoing and is_op_adjacent_to_next_delete and next_change.op.d.slice(0, op.i.length) == op.i

 					# If there is a delete at the start of the insert, and we're inserting
 					# at the start, we SHOULDN'T merge since the delete acts as a partition.
@@ -281,8 +314,8 @@
 			for change in remove_changes
 				@_removeChange change

-			if moved_changes.length > 0
-				@emit "changes:moved", moved_changes
+			for change in moved_changes
+				@_markAsDirty change, "change", "moved"

 		applyDeleteToChanges: (op, metadata) ->
 			op_start = op.p
@@ -406,8 +439,8 @@
 					@_removeChange change
 					moved_changes = moved_changes.filter (c) -> c != change

-			if moved_changes.length > 0
-				@emit "changes:moved", moved_changes
+			for change in moved_changes
+				@_markAsDirty change, "change", "moved"

 		_addOp: (op, metadata) ->
 			change = {
@@ -427,17 +460,11 @@
 				else
 					return -1

-			if op.d?
-				@emit "delete:added", change
-			else if op.i?
-				@emit "insert:added", change
+			@_markAsDirty(change, "change", "added")

 		_removeChange: (change) ->
 			@changes = @changes.filter (c) -> c.id != change.id
-			if change.op.d?
-				@emit "delete:removed", change
-			else if change.op.i?
-				@emit "insert:removed", change
+			@_markAsDirty change, "change", "removed"

 		_applyOpModifications: (content, op_modifications) ->
 			# Put in descending position order, with deleting first if at the same offset
@@ -486,13 +513,32 @@
 					previous_change = change
 			return { moved_changes, remove_changes }

+		resetDirtyState: () ->
+			@_dirtyState = {
+				comment: {
+					moved: {}
+					removed: {}
+					added: {}
+				}
+				change: {
+					moved: {}
+					removed: {}
+					added: {}
+				}
+			}
+
+		getDirtyState: () ->
+			return @_dirtyState
+
+		_markAsDirty: (object, type, action) ->
+			@_dirtyState[type][action][object.id] = object
+
 		_clone: (object) ->
 			clone = {}
 			(clone[k] = v for k,v of object)
 			return clone

 if define?
-	define ["utils/EventEmitter"], load
+	define [], load
 else
-	EventEmitter = require("events").EventEmitter
-	module.exports = load(EventEmitter)+	module.exports = load()


Fixed crash when imageSize isn't present

--- src/core/LiterallyCanvas.coffee
+++ src/core/LiterallyCanvas.coffee
@@ -52,8 +52,8 @@
     util.matchElementSize(
       @containerEl, [@canvas], @backingScale, => @repaintLayer('main'))

-    width = opts.imageSize.width
-    height = opts.imageSize.height
+    width = if opts.imageSize then opts.imageSize.width else undefined
+    height = if opts.imageSize then opts.imageSize.height else undefined

     # TODO: DRY these up
     if width == 'infinite' or not width


wording update

wording update

--- src/course/enroll-or-login.cjsx
+++ src/course/enroll-or-login.cjsx
@@ -25,7 +25,7 @@

           {newToConceptCoach}  <LoginGateway className="sign-up">
-            Sign up with enrollment code
+            Sign up with your enrollment code
           </LoginGateway>


:bug: Fix rangeFromLineNumber Ranges are 0-based, not 1-based

--- lib/helpers.coffee
+++ lib/helpers.coffee
@@ -61,8 +61,8 @@
     unless typeof colStart is 'number'
       colStart = (textEditor.indentationForBufferRow(lineNumber) * textEditor.getTabLength())
     return [
-      [lineNumber, colStart],
-      [lineNumber, textEditor.getBuffer().lineLengthForRow(lineNumber)]
+      [lineNumber, colStart - 1],
+      [lineNumber, textEditor.getBuffer().lineLengthForRow(lineNumber) - 1]
     ]

   # Due to what we are attempting to do, the only viable solution right now is


don't send multiple copies of a message with multiple clicks

test plan:
 - try to click Send multiple times after composing a message
 - it should only send one copy
   (after clicking it the first time, the Send button will be
    disabled and its text will read "Sending...")

fixes CNVS-38923

Change-Id: I2c79bdbfc27289924b73411f50e7f1b8692b5adf
Reviewed-on: https://gerrit.instructure.com/124676
Tested-by: Jenkins
Reviewed-by: Venk Natarajan <f9eab59b292314637a3c917fea4742741e36a19b@instructure.com>
QA-Review: David Tan <6283e310a2278fc51bdc4fb5974f70dd9c3366b9@instructure.com>
Product-Review: Jeremy Stanley <b3f594e10a9edcf5413cf1190121d45078c62290@instructure.com>

--- app/coffeescripts/views/conversations/MessageFormDialog.coffee
+++ app/coffeescripts/views/conversations/MessageFormDialog.coffee
@@ -89,6 +89,7 @@
         'data-track-category': "Compose Message"
         'data-track-action'  : "Edit"
         'data-track-label'   : "Send"
+        'data-text-while-loading' : I18n.t('Sending...')
         click: (e) => @sendMessage(e)
       ]

@@ -277,7 +278,6 @@
         folder_id: @options.folderId
         intent: 'message'
         formDataTarget: 'url'
-        disableWhileLoading: true
         required: ['body']
         property_validations:
           token_capture: => I18n.t("Invalid recipient name.") if @recipientView and !@recipientView.tokens.length
@@ -289,6 +289,7 @@
           formData
         onSubmit: (@request, submitData) =>
           dfd = $.Deferred()
+          $(@el).parent().disableWhileLoading(dfd, buttons: ['[data-text-while-loading] .ui-button-text']);
           @trigger('submitting', dfd)
           # update conversation when message confirmed sent
           # TODO: construct the new message object and pass it to the MessageDetailView which will need to create a MessageItemView for it


Social: fix forgotten white space before closing bracket

--- workers/social/lib/social/models/socialapi/channel.coffee
+++ workers/social/lib/social/models/socialapi/channel.coffee
@@ -172,7 +172,7 @@
   @fetchGroupActivities = secure (client, options, callback)->
     ensureGroupChannel client, (err, socialApiChannelId)->
       return callback err if err
-      return callback { message: "Channel Id is not set"} unless socialApiChannelId
+      return callback { message: "Channel Id is not set" } unless socialApiChannelId

       options.id = socialApiChannelId
       SocialChannel.fetchActivities client, options, callback


Allow observing [] for all keys

Allow observing [] for all keys

--- lib/observe.coffee
+++ lib/observe.coffee
@@ -1,6 +1,6 @@
 module.exports = (observed, keys, callback) ->
     Object.observe observed, (changes) =>
       for change in changes
-        if change.name in keys
+        if (change.name in keys) or (keys.length is 0)
           callback()
           break


remove initializing category

remove initializing category

--- lib/payload.coffee
+++ lib/payload.coffee
@@ -13,7 +13,6 @@
         @data = {}
         @var = {}
         @incrementBadge = yes
-        @category = {}
         @contentAvailable = false

         # Read fields


Fix error that easy happens

Fix error that easy happens

--- lib/pg-hoff.coffee
+++ lib/pg-hoff.coffee
@@ -149,9 +149,15 @@
         if (alias)
             pgHoff.executeQuery()
         else
-            @connect()
-                .then ->
-                    pgHoff.executeQuery()
+            promise = @connect()
+            if not promise?
+                return
+            else
+                promise
+                    .then ->
+                        pgHoff.executeQuery()
+                    .catch (err) ->
+                        atom.notifications.addError('Connect error')
     executeQuery: ->
         selectedText = atom.workspace.getActiveTextEditor().getSelectedText().trim()
         pgHoff = @


Minimize render

Minimize render

--- lib/pg-hoff.coffee
+++ lib/pg-hoff.coffee
@@ -79,18 +79,6 @@
         @subscriptions.add atom.commands.add 'atom-workspace', 'pg-hoff:connect': => @connect()
         @subscriptions.add atom.commands.add 'atom-workspace', 'pg-hoff:execute-query': => @executeQueryWithConnect()

-    deactivate: ->
-        @subscriptions.dispose()
-        @pgHoffView.destroy()
-        @resultsView.destroy()
-        @listServersView.destroy()
-
-    serialize: ->
-        pgHoffViewState: @pgHoffView.serialize()
-
-    provide: ->
-        @provider
-
     connect: ->
         paneItem = atom.workspace.getActivePaneItem()

@@ -98,7 +86,6 @@
             @listServersViewPanel.hide()
             return

-        pgHoff = @
         return @listServersView.connect(@listServersViewPanel)
             .then (server) =>
                 if server.already_connected
@@ -112,34 +99,9 @@
             .finally =>
                 @listServersViewPanel.hide()

-    renderResults: (resultsets, pgHoff) ->
+    renderResults: (resultsets) ->
         @resultsViewPanel.show();
         @resultsView.update(resultsets)
-
-    keepGoing: (url, complete, pgHoff) ->
-        interval = setInterval( () =>
-            return PgHoffServerRequest.Get(url, false)
-                .then (resultsets) =>
-                    for resultset, i in resultsets
-                        if resultset.error
-                            complete[i] = true
-                            atom.notifications.addError(resultset.error)
-                        else
-                            if resultset.executing
-                                complete[i] = false
-                            else
-                                if complete[i] == false
-                                    complete[i] = true
-
-                    @renderResults(resultsets, pgHoff)
-                    if !complete.some( (val) -> return val == false )
-                        clearInterval(interval)
-
-                .catch (err) =>
-                    console.error 'catch', err
-                    clearInterval(interval)
-                    atom.notifications.addError(err)
-        , atom.config.get('pg-hoff.pollInterval'))

     executeQueryWithConnect: ->
         if atom.workspace.getActivePaneItem().alias?
@@ -153,7 +115,6 @@

     executeQuery: ->
         selectedText = atom.workspace.getActiveTextEditor().getSelectedText().trim()
-        pgHoff = @
         if selectedText.trim().length == 0
             if atom.config.get('pg-hoff.executeAllWhenNothingSelected')
                 selectedText = atom.workspace.getActiveTextEditor().getText().trim()
@@ -163,23 +124,51 @@

         return PgHoffQuery.Execute(selectedText)
             .then (result) =>
-                complete = []
-                queryStillExecuting = false
                 for resultset in result.resultsets
                     if resultset.error
-                        complete.push(true)
                         atom.notifications.addError(resultset.error)
-                    else
-                        if resultset.executing
-                            complete.push(false)
-                            queryStillExecuting = true
-                        else
-                            complete.push(true)
-                if queryStillExecuting
-                    @keepGoing(result.url, complete)
+
+                allCompleted = result.resultsets.every (resultset) -> return resultset.completed or resultset.error or not resultset.executing
+                if not allCompleted
+                    @keepGoing(result.url)

                 @renderResults(result.resultsets)
             .catch (err) =>
                 atom.workspace.getActivePaneItem().alias = null
                 @resultsViewPanel.hide()
                 atom.notifications.addError(err)
+
+    keepGoing: (url, complete) ->
+        interval = setInterval( () =>
+            return PgHoffServerRequest.Get(url, false)
+                .then (resultsets) =>
+                    for resultset, i in resultsets
+                        if resultset.error
+                            atom.notifications.addError(resultset.error)
+
+                    count = resultsets.find (resultset) -> resultset.completed or resultset.error or not resultset.executing
+                    if completedCount != count
+                        @renderResults(resultsets)
+                        completedCount = count
+
+                    allCompleted = resultsets.every (resultset) -> resultset.completed or resultset.error or not resultset.executing
+                    if allCompleted
+                        clearInterval(interval)
+
+                .catch (err) =>
+                    console.error 'catch', err
+                    clearInterval(interval)
+                    atom.notifications.addError(err)
+        , atom.config.get('pg-hoff.pollInterval'))
+
+    deactivate: ->
+        @subscriptions.dispose()
+        @pgHoffView.destroy()
+        @resultsView.destroy()
+        @listServersView.destroy()
+
+    serialize: ->
+        pgHoffViewState: @pgHoffView.serialize()
+
+    provide: ->
+        @provider


Fixes a typo (missing closing double quote)

--- src/scripts/defprogramming.coffee
+++ src/scripts/defprogramming.coffee
@@ -3,7 +3,7 @@
 #
 # Dependencies:
 #   "htmlparser": "1.7.6"
-#   "soupselect: "0.2.0"
+#   "soupselect": "0.2.0"
 #
 # Configuration:
 #   None


remove load and fetchPage

remove load and fetchPage

--- src/scripts/models/content.coffee
+++ src/scripts/models/content.coffee
@@ -41,7 +41,10 @@
           @set('loaded', true)
         .done () =>
           @set('error', false)
-          @load(options.page)
+          if @isBook()
+            if @get('contents').length
+              @setPage(options.page or 1) # Default to page 1
+
         .fail (model, response, options) =>
           @set('error', response?.status or model?.status or 9000)

@@ -74,16 +77,6 @@

       return results

-    load: (page) ->
-      if @isBook()
-        if @get('contents').length
-          @setPage(page or 1) # Default to page 1
-
-    fetchPage: () ->
-      page = @get('currentPage')
-      page.fetch().done () =>
-        page.set('loaded', true)
-
     setPage: (num) ->
       pages = @getTotalPages()

@@ -97,7 +90,8 @@
       @trigger('changePage')

       if not page.get('loaded')
-        @fetchPage()
+        page.fetch().done () =>
+          page.set('loaded', true)

     getTotalPages: () ->
       # FIX: cache total pages and recalculate on add/remove events?


change `command` to `cmd`

change `command` to `cmd`

elliot why

--- lib/proscli.coffee
+++ lib/proscli.coffee
@@ -143,7 +143,7 @@
         wait 75 # hard code waits to allow commands to be executed
         currentTerminal.insertSelection(if navigator.platform is 'Win32' then 'cls' else 'clear')
         wait 75
-        currentTerminal.insertSelection command.join ' '
+        currentTerminal.insertSelection cmd.join ' '
         currentTerminal.focus()
       else
         currentTerminal = terminalService.run([cmd.join ' '])[0].spacePenView


Cleanup.

Cleanup.

--- lib/jumpy-view.coffee
+++ lib/jumpy-view.coffee
@@ -100,6 +100,5 @@
             .html(key)
             .addClass(key)

-
   clear: ->
       @clearJumpMode()


show user's map when mouseover to user's picture

--- app/assets/javascripts/map.coffee
+++ app/assets/javascripts/map.coffee
@@ -10,14 +10,12 @@
         zoom: 12
         mapTypeId: google.maps.MapTypeId.ROADMAP

-      if data.length > 0
-        mapOptions.center = LL(data[0].lat, data[0].lon)
-        vuser = data[0]
+      vuser = data[0] if data.length > 0

+      that.users = []
       for user in data
-        if user.id == that.options.user_id
-          mapOptions.center = LL(user.lat, user.lon)
-          vuser = user
+        that.users[user.id] = user
+        vuser = user if user.id is that.options.user_id

       h = $('body > .container-fluid').height() - $('#menu').height()
       $('#listview').css('height', h)
@@ -38,39 +36,12 @@
               animation: google.maps.Animation.DROP

             google.maps.event.addListener marker, 'click', (event) ->
-              that.map.setCenter LL(s.lat, s.lon)
-              that.sv.getPanoramaByLocation LL(s.lat, s.lon), 100, (data, stat) ->
-                if stat is google.maps.StreetViewStatus.OK
-                  that.pano.setPano data.location.pano
-                  that.pano.setPov
-                    heading: if s.bearing then s.bearing else 0
-                    pitch: 0
-                    zoom: 1
-                  that.pano.setVisible true
-                else
-                  that.pano.setVisible false
-              that.pano.setPosition LL(s.lat, s.lon)
+              that.setUserMap s
           , i * 100
-
-      panoramaOptions =
-        position: LL(vuser.lat, vuser.lon)
-        pov:
-          heading: if vuser.bearing then vuser.bearing else 0
-          pitch: 0
-          zoom: 1

       that.pano = new google.maps.StreetViewPanorama($('#panorama')[0])
       that.sv = new google.maps.StreetViewService()
-      that.sv.getPanoramaByLocation LL(vuser.lat, vuser.lon), 100, (data, stat) ->
-        if stat is google.maps.StreetViewStatus.OK
-          that.pano.setPano data.location.pano
-          that.pano.setPov
-            heading: if vuser.bearing then vuser.bearing else 0
-            pitch: 0
-            zoom: 1
-          that.pano.setVisible true
-        else
-          that.pano.setVisible false
+      that.setUserMap vuser

       $(window).resize ->
         h = $('body > .container-fluid').height() - $('#menu').height()
@@ -81,6 +52,29 @@
         else
           $('#map').css('height', h - $('#panorama').height())
         google.maps.event.trigger that.map, 'resize'
+
+      $('#listview').on 'mouseover', 'img', ->
+        uid = $(@).parents('tr').data('user-id')
+        that.setUserMap that.users[uid]
+
+  that.setUserMap = (user) ->
+    that.sv.getPanoramaByLocation LL(user.lat, user.lon), 100, (data, stat) ->
+      if stat is google.maps.StreetViewStatus.OK
+        that.pano.setPosition LL(user.lat, user.lon)
+        that.pano.setPov
+          heading: if user.bearing then user.bearing else 0
+          pitch: 0
+          zoom: 1
+        $('#panorama').show()
+        that.pano.setVisible true
+        $('#map').css('height', $('#mapview').height() - $('#panorama').height())
+        google.maps.event.trigger that.map, 'resize'
+      else
+        $('#panorama').hide()
+        that.pano.setVisible false
+        $('#map').css('height', $('#mapview').height())
+        google.maps.event.trigger that.map, 'resize'
+      that.map.setCenter LL(user.lat, user.lon)

   that.overlay_kml = (kml_url) ->
     console.log("load KML: " + kml_url)


Show left side [f] indicator

Show left side [f] indicator

--- lib/psc-ide.coffee
+++ lib/psc-ide.coffee
@@ -55,5 +55,6 @@
               text: x.text
               displayText: x.text + ": " + x.type
               description: x.type
+              type: if /->/.test(x.type) then "function" else "value"

 module.exports = PscIde


removes verbose docs for private method

removes verbose docs for private method

--- lib/publish.coffee
+++ lib/publish.coffee
@@ -24,14 +24,9 @@

   # private

-  # parse params to find the filename and publication name
-  # splits `posts:userPosts` into ['posts:userPosts','posts','userPosts']
-  #
-  # params - single {String} formatted with colon seperation
-  # returns {Object}
-  #     : filenameRoot - {String} filename minus extension
-  #     : pubName - {String} name of meteor publication
+  # find the filename and publication name
   parseParams:(params) ->
+    # splits `posts:userPosts` into ['posts:userPosts','posts','userPosts']
     matches = params.match(/^([^:]+):([^:]+)$/)
     if !matches[1] || !matches[2]
       throw new Error("Bad Publish Name Syntax, see mgen publish --help")


ImpacKpisSvc#show applies ssoSession to params if exists

--- src/services/kpis/kpis.svc.coffee
+++ src/services/kpis/kpis.svc.coffee
@@ -157,7 +157,7 @@
       _self.load().then(
         ->
           params = {}
-          params.sso_session = _self.getSsoSessionId()
+          params.sso_session = _self.getSsoSessionId() if _self.getSsoSessionId()
           params.targets = kpi.targets if kpi.targets?
           params.metadata = kpi.settings if kpi.settings?
           params.extra_params = kpi.extra_params if kpi.extra_params?


compute client: fix for race condition revealed by production

--- src/smc-hub/compute-client.coffee
+++ src/smc-hub/compute-client.coffee
@@ -888,7 +888,12 @@
                     @_stale = false
                     @_synctable = x
                     update = () =>
-                        new_val = @_synctable.get(@project_id).toJS()
+                        new_val       = @_synctable.get(@project_id)?.toJS()
+                        if not new_val?
+                            # The project hasn't been created/written to the database yet, in which
+                            # case there is nothing to do.  It should get added in a moment and
+                            # trigger another update.
+                            return
                         old_host      = @host
                         @host         = new_val.host?.host
                         @assigned     = new_val.host?.assigned


fix paths issue

fix paths issue

--- lib/pandoc-wrapper.coffee
+++ lib/pandoc-wrapper.coffee
@@ -109,20 +109,14 @@

   Object.assign({}, data, config)

-processConfigPaths = (config, outputDir, projectDirectoryPath, rootDirectoryPath)->
+processConfigPaths = (config, rootDirectoryPath, projectDirectoryPath)->
   # same as the one in processPaths function
   # TODO: refactor in the future
   resolvePath = (src)->
-    if src.startsWith('http://') or
-       src.startsWith('https://') or
-       src.startsWith('atom://') or
-       src.startsWith('file://') or
-       src.startsWith('#')
+    if src.startsWith('/')
+      return path.relative(rootDirectoryPath, path.resolve(projectDirectoryPath, '.'+src))
+    else # ./test.png or test.png
       return src
-    else if src.startsWith('/')
-      return path.relative(outputDir, path.resolve(projectDirectoryPath, '.'+src))
-    else # ./test.png or test.png
-      return path.relative(outputDir, path.resolve(rootDirectoryPath, src))

   helper = (data)->
     if typeof(data) == 'string'
@@ -155,22 +149,16 @@
     if outputConfig['template']
       outputConfig['template'] = helper(outputConfig['template'])

-processPaths = (text, outputDir, projectDirectoryPath, rootDirectoryPath)->
+processPaths = (text, rootDirectoryPath, projectDirectoryPath)->
   match = null
   offset = 0
   output = ''

   resolvePath = (src)->
-    if src.startsWith('http://') or
-       src.startsWith('https://') or
-       src.startsWith('atom://') or
-       src.startsWith('file://') or
-       src.startsWith('#')
+    if src.startsWith('/')
+      return path.relative(rootDirectoryPath, path.resolve(projectDirectoryPath, '.'+src))
+    else # ./test.png or test.png
       return src
-    else if src.startsWith('/')
-      return path.relative(outputDir, path.resolve(projectDirectoryPath, '.'+src))
-    else # ./test.png or test.png
-      return path.relative(outputDir, path.resolve(rootDirectoryPath, src))

   # replace path in ![](...) and []()
   r = /(\!?\[.*?]\()([^\)|^'|^"]*)(.*?\))/gi
@@ -239,7 +227,7 @@
     args.push '-o', outputFilePath

   # resolve paths in front-matter(yaml)
-  processConfigPaths config, path.dirname(outputFilePath), md.projectDirectoryPath, md.rootDirectoryPath
+  processConfigPaths config, md.rootDirectoryPath, md.projectDirectoryPath

   if outputConfig
     processOutputConfig outputConfig, args
@@ -248,11 +236,11 @@
   text = matter.stringify(text, config)

   # change link path to relative path
-  text = processPaths text, path.dirname(outputFilePath), md.projectDirectoryPath, md.rootDirectoryPath
+  text = processPaths text, md.rootDirectoryPath, md.projectDirectoryPath

   # change working directory
   cwd = process.cwd()
-  process.chdir(path.dirname(outputFilePath))
+  process.chdir(md.rootDirectoryPath)

   # citation
   if config['bibliography'] or config['references']


hack so compute-server can be imported

hack so compute-server can be imported

--- src/smc-hub/compute-server.coffee
+++ src/smc-hub/compute-server.coffee
@@ -1236,6 +1236,9 @@
 # Command line interface
 ###########################

+if not program._name
+    program._name = 'x'  # so this module is possible to import with latest node version
+
 program.usage('[start/stop/restart/status] [options]')
     .option('--pidfile [string]',        'store pid in this file', String, "#{CONF}/compute.pid")
     .option('--logfile [string]',        'write log to this file', String, "#{CONF}/compute.log")


reset artifact selector when exitting artifactTool

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -141,7 +141,8 @@
         @el.find('.artifact-classifier').hide()
         @doneButton.hide()
         @finishButton.show()
-
+        el.checked = false for el in [ @artifactSelector ... ] # reset artifact selector
+
   constructor: ->
     super
     @asteroidMarkedInFrame = [ null, null, null, null ]
@@ -165,7 +166,6 @@
     Subject.on 'select', @onSubjectSelect

   onSelectArtifact: ->
-    console.log "ARTIFACT TYPE: ", @artifactSelector.filter(':checked').val()
     @currSighting.subType = @artifactSelector.filter(':checked').val()

   createMarkingSurfaces: ->


unbind talk event

unbind talk event

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -113,6 +113,9 @@
     Subject.off 'fetch', @appendSubjects
     Subject.off 'select', @onSubjectSelect
     Subject.off 'no-more', @onNoMoreSubjects
+
+    # Unbind Talk event
+    @unbind 'addToTalk'

     # Set default counts
     @nClassified  = 0


:beers: stop mouse event propagation for provider-panel

--- lib/provider-panel.coffee
+++ lib/provider-panel.coffee
@@ -2,6 +2,10 @@

 toggleSelected = (element, bool) ->
   element.classList.toggle('selected', bool)
+
+suppressEvent: (event) ->
+  event.preventDefault()
+  event.stopPropagation()

 # This is NOT Panel in Atom's terminology, Just naming.
 module.exports =
@@ -48,6 +52,10 @@
     @ui.grammar.onDidChangeSearchTerm (regexp) ->
       searchTermElement.textContent = regexp?.toString() ? ''

+    # NOTE: Avoid mousedown event propagated up to belonging narrow-editor's element
+    # If propagated, button clicking cause narrow-editor's cursor move etc See #123.
+    @container.addEventListener('mousedown', suppressEvent)
+
     # searchOptions
     [@ignoreCaseButton, @wholeWordButton] = element.getElementsByTagName('button')
     @ignoreCaseButton.addEventListener('click', @toggleSearchIgnoreCase)
@@ -71,8 +79,13 @@
     toggleSelected(@ignoreCaseButton, @provider.searchIgnoreCase)
     toggleSelected(@wholeWordButton, @provider.searchWholeWord)

-  toggleSearchIgnoreCase: => @ui.toggleSearchIgnoreCase()
-  toggleSearchWholeWord: => @ui.toggleSearchWholeWord()
+  toggleSearchIgnoreCase: (event) =>
+    suppressEvent(event)
+    @ui.toggleSearchIgnoreCase()
+
+  toggleSearchWholeWord: (event) =>
+    suppressEvent(event)
+    @ui.toggleSearchWholeWord()

   activateSearchOptionButtonToolTips: ->
     @toolTipDisposables?.dispose()


Removed default config in the "basic" case, as that's a bit dangerous. Might cause unexpected behaviour if a config is simply forgotten

--- app/js/loader/loader_basic.coffee
+++ app/js/loader/loader_basic.coffee
@@ -1,6 +1 @@
-window.FactlinkConfig ?= {}
-window.FactlinkConfig.api ?= 'https://factlink.com:443'
-window.FactlinkConfig.lib ?= 'https://static.factlink.com:443/lib/dist'
-window.FactlinkConfig.srcPath ?= '/factlink.core.min.js'
-
 window.FACTLINK_START_LOADER()


Tweak vomnibar custom search behaviour.

Tweak vomnibar custom search behaviour.

With this, if we're in custom search mode and the cursor is at the start
of the vomnibar input and the user types <Delete>, then we exit custom
search mode and reinstate the keyword (and arrange for the cursor to be
in the right place).

This makes it possible to change (or just disable) the custom search
engine without having to first wholly delete the query.

--- pages/vomnibar.coffee
+++ pages/vomnibar.coffee
@@ -169,14 +169,14 @@
         completion = @completions[@selection]
         @hide -> completion.performAction openInNewTab
     else if action == "delete"
-      inputIsEmpty = @input.value.length == 0
-      if inputIsEmpty and @customSearchMode?
+      if @customSearchMode? and @input.selectionEnd == 0
         # Normally, with custom search engines, the keyword (e,g, the "w" of "w query terms") is suppressed.
-        # If the input is empty, then reinstate the keyword (the "w").
-        @input.value = @customSearchMode
+        # If the cursor is at the start of the input, then reinstate the keyword (the "w").
+        @input.value = @customSearchMode + @input.value.ltrim()
+        @input.selectionStart = @input.selectionEnd = @customSearchMode.length
         @customSearchMode = null
         @update true
-      else if inputIsEmpty and @seenTabToOpenCompletionList
+      else if @seenTabToOpenCompletionList and @input.value.trim().length == 0
         @seenTabToOpenCompletionList = false
         @update true
       else


Fixed a typo on a fat arrow

Fixed a typo on a fat arrow

--- lib/system-checker.coffee
+++ lib/system-checker.coffee
@@ -40,7 +40,7 @@
     if @isEnabled()
       # We use the default checker here and not the locale-specific one so it
       # will check all languages at the same time.
-      instance.checkSpellingAsync(text).then (incorrect) ->
+      instance.checkSpellingAsync(text).then (incorrect) =>
         @log 'check', text, incorrect
         {id, invertIncorrectAsCorrect: true, incorrect}
     else


Buttons priority

Buttons priority

--- lib/util/lifecycle.coffee
+++ lib/util/lifecycle.coffee
@@ -74,23 +74,28 @@
       icon: 'gear',
       callback: 'advanced-web-editor:configure',
       tooltip: 'Configure'
-
-    toolBar.addSpacer()
+      priority: 86
+
+    toolBar.addSpacer
+      priority: 87

     startBtn = toolBar.addButton
       icon: 'zap',
       callback: 'advanced-web-editor:start',
       tooltip: 'Start Editing'
+      priority: 88

     saveBtn = toolBar.addButton
       icon: 'database',
       callback: 'advanced-web-editor:save',
       tooltip: 'Save Locally'
+      priority: 89

     publishBtn = toolBar.addButton
       icon: 'cloud-upload',
       callback: 'advanced-web-editor:publish',
       tooltip: 'Publish'
+      priority: 90

     startBtn.setEnabled @status == STATUS.READY
     saveBtn.setEnabled @status == STATUS.STARTED


Only parse events that exist

Only parse events that exist

--- lib/views/terminal.coffee
+++ lib/views/terminal.coffee
@@ -78,7 +78,7 @@
     @terminal.on 'terminal-session-opened', () =>
       @term.off 'data'
       @term.on 'data', (data) =>
-        if !!process.platform.match(/^win/)
+        if !!process.platform.match(/^win/) && event?
           if event.which == 67 && event.shiftKey && event.ctrlKey
             @copy()
           else if event.which == 86 && event.shiftKey && event.ctrlKey


Fix repeating vparam execution

Fix repeating vparam execution

This issue only happens when a device doesn't maintain a single
connection for the entire session.

--- lib/session.coffee
+++ lib/session.coffee
@@ -1049,15 +1049,20 @@
   provisions = null
   for declaration in virtualParameterDeclarations
     if declaration[1]
-      provisions ?= []
       currentTimestamps = {}
       currentValues = {}
+      dec = {}
       attrs = sessionContext.deviceData.attributes.get(declaration[0])
+      for k, v of declaration[1]
+        continue if k != 'value' and k != 'writable'
+        if not (v < attrs[k][0])
+          dec[k] = v
       for k, v of attrs
         currentTimestamps[k] = v[0]
         currentValues[k] = v[1]
-      provisions.push([declaration[0][1], declaration[1], {}, currentTimestamps, currentValues])
-      delete declaration[1]
+      if (Object.keys(dec).length)
+        provisions ?= []
+        provisions.push([declaration[0][1], dec, {}, currentTimestamps, currentValues])

   return provisions


Modify argument checking logic at Tracker#set and get

--- lib/tracker.coffee
+++ lib/tracker.coffee
@@ -19,7 +19,7 @@
       .on 'tracker', (data)->
         handleData.call tracker, data
       .on 'connect', (data)->
-        tracker.set _.pick(tracker.config, Protocol.MUTABLE_CONFIG_KEYS)
+        tracker.set tracker.config
         tracker.emit 'connect'
       .on 'disconnect', (code, reason)->
         tracker.emit 'disconnect', code, reason
@@ -41,6 +41,7 @@

   set: (values)->
+    values = _.pick values, Protocol.MUTABLE_CONFIG_KEYS
     @connection.send
       category: 'tracker'
       request: 'set'
@@ -48,7 +49,7 @@
     @

   get: (keys)->
-    keys = [ keys ] unless Array.isArray keys
+    keys = [ keys ] if _.isString keys
     @connection.send
       category: 'tracker'
       request: 'get'


Fix bug when only one data item available

--- _includes/simulation.coffee
+++ _includes/simulation.coffee
@@ -507,9 +507,10 @@
     vega_data = vega_data[1..]

   with_div = (x) -> x.append('div').attr('class', 'col s4')
-  add_card(add_image, '#images', with_div = with_div)(result_data.image)
-  # with_div = (x) -> x.append('div').attr('class', 'col s4')
+  if result_data.image?
+    add_card(add_image, '#images', with_div = with_div)(result_data.image)
   add_card(add_chart, '#images', with_div = with_div)(vega_data)
-  add_card(add_youtube, '#youtube', with_div = id)(result_data.youtube[0..0])
+  if result_data.youtube?
+    add_card(add_youtube, '#youtube', with_div = id)(result_data.youtube[0..0])

 build(DATA, SIM_NAME, CODES_DATA, CHART_DATA)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera.coffee
+++ src/camera.coffee
@@ -108,7 +108,7 @@

       objects.invoke "trigger", "overlay", canvas

-  self.include Movable
+  self.include(Movable)

   self.include(Camera.ZSort)
   self.include(Camera.Zoom)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera.coffee
+++ src/camera.coffee
@@ -7,7 +7,7 @@
       height: App.height
     screen: Rectangle
       x: 0
-      y: App.height / 2
+      y: 0
       width: App.width
       height: App.height
     deadzone: Point(0, 0)


fixed eachR and bindR methods

fixed eachR and bindR methods

--- manager/lib/assets/javascripts/underscore_patch.js.coffee
+++ manager/lib/assets/javascripts/underscore_patch.js.coffee
@@ -10,11 +10,11 @@
   @each func, context

 # eachR - allows passing the context as the first param instead of last
-_.eachR = (context, func) ->
-  @each func, context
+_.eachR = (context, list, func) ->
+  @each list, func, context

-_.bindR = (context, func) ->
-  @bind func, context
+_.bindR = (context, func, args...) ->
+  @bind func, context, args...

 # alias away the sync method
 Backbone._sync = Backbone.sync


Fix typo in getUnreadCount and getChannelsWithUnreads

--- src/client.coffee
+++ src/client.coffee
@@ -307,7 +307,7 @@
     for id, channel of @channels
       if channel.unread_count? then count += channel.unread_count

-    for id, dm of @ims
+    for id, dm of @dms
       if dm.unread_count? then count += dm.unread_count

     for id, group of @groups
@@ -320,7 +320,7 @@
     for id, channel of @channels
       if channel.unread_count > 0 then unreads.push channel

-    for id, dm of @ims
+    for id, dm of @dms
       if dm.unread_count > 0 then unreads.push dm

     for id, group of @groups


Rudimentary object literal parser support

--- src/coffee.coffee
+++ src/coffee.coffee
@@ -99,6 +99,22 @@
             start: [node.locationData.first_line, node.locationData.first_column]
             end: end
           }
+        when 'Obj'
+          start = [node.locationData.first_line, node.locationData.first_column]
+          end = [node.locationData.last_line, node.locationData.last_column + 1]
+
+          firstChildBounds = getBounds node.properties[0]
+
+          if firstChildBounds.start[0] <= start[0]
+            start = [firstChildBounds.start[0] - 1, text[firstChildBounds.start[0] - 1].length]
+
+          if text[end[0]][...end[1]].trimLeft().length is 0
+            end[0] -= 1; end[1] = text[end[0]].length
+
+          return {
+            start: start
+            end: end
+          }

         else
           end = [node.locationData.last_line, node.locationData.last_column + 1]
@@ -229,10 +245,10 @@
           end = getBounds node.properties[node.properties.length - 1]

           indent = new model.Indent 2
-
+
           markup.push
             token: indent.start
-            position: start.start
+            position: [start.start[0]-1, text[start.start[0]-1].length]
             id: id
             start: true


getKpiTemplate method now uses endpoint & watchable

--- src/services/kpis/kpis.svc.coffee
+++ src/services/kpis/kpis.svc.coffee
@@ -19,8 +19,10 @@
     @getKpisTemplates = ->
       return _self.config.kpisTemplates

-    @getKpiTemplate = (endpoint)->
-      return _.find(_self.getKpisTemplates(), (k)-> k.endpoint == endpoint )
+    @getKpiTemplate = (endpoint, primaryWatchable)->
+      return _.find(_self.getKpisTemplates(), (k)->
+        k.endpoint == endpoint && k.watchables[0] == primaryWatchable
+      )

     @getAttachableKpis = (widgetEngine) ->
       _self.load().then(->
@@ -170,7 +172,7 @@
     # unit depending on the watchable. Impac Angular forces the mode & unit, and suggests the
     # value in the target input placeholder.
     @getKpiTargetPlaceholder = (kpiEndpoint, kpiWatchable) ->
-      templ = _self.getKpiTemplate(kpiEndpoint)
+      templ = _self.getKpiTemplate(kpiEndpoint, kpiWatchable)
       ((templ? && templ.target_placeholders?) && templ.target_placeholders[kpiWatchable]) || {}

     # TODO: mno & impac should be change to deal with `watchables`, instead


Allow other machines to access to server

--- lib/broadcast-server.coffee
+++ lib/broadcast-server.coffee
@@ -51,7 +51,7 @@
             console.log req.url
             console.log err
       .resume()
-    .listen port, hostname
+    .listen port, '0.0.0.0'

     @server.addListener 'connection', (socket) =>
       @sockets.push socket


Fix draggable/pinachable weird offsets

Fix draggable/pinachable weird offsets

--- framer/LayerPinchable.coffee
+++ framer/LayerPinchable.coffee
@@ -41,28 +41,40 @@
 		@layer.on(Gestures.PinchStart, @_pinchStart)
 		@layer.on(Gestures.Pinch, @_pinch)
 		@layer.on(Gestures.PinchEnd, @_pinchEnd)
+		@layer.on(Gestures.TapStart, @_tapStart)

 	_reset: ->
 		@_scaleStart = null
 		@_rotationStart = null
 		@_rotationOffset = null

+	_tapStart: (event) ->
+		#@_centerOrigin(event) if @centerOrigin
+
+	_centerOrigin: (event) =>
+
+		topInSuperBefore = Utils.convertPoint({}, @layer, @layer.superLayer)
+		pinchLocation = Utils.convertPointFromContext(event.touchCenter, @layer, true, true)
+		@layer.originX = pinchLocation.x / @layer.width
+		@layer.originY = pinchLocation.y / @layer.height
+
+		topInSuperAfter = Utils.convertPoint({}, @layer, @layer.superLayer)
+		xDiff = topInSuperAfter.x - topInSuperBefore.x
+		yDiff = topInSuperAfter.y - topInSuperBefore.y
+		@layer.x -= xDiff
+		@layer.y -= yDiff
+
+		# This is not a great fix, we should add this in the draggable. Basically
+		# we need to account in the draggable for a change of origin relative to
+		# the dragging start offset.
+
+		if @layer._draggable
+			@layer.draggable._layerStartPoint.x -= xDiff
+			@layer.draggable._layerStartPoint.y -= yDiff
+
 	_pinchStart: (event) =>
-
 		@_reset()
-
-		if @centerOrigin
-
-			topInSuperBefore = Utils.convertPoint({}, @layer, @layer.superLayer)
-			pinchLocation = Utils.convertPointFromContext(event.touchCenter, @layer, true, true)
-			@layer.originX = pinchLocation.x / @layer.width
-			@layer.originY = pinchLocation.y / @layer.height
-
-			topInSuperAfter = Utils.convertPoint({}, @layer, @layer.superLayer)
-			xDiff = topInSuperAfter.x - topInSuperBefore.x
-			yDiff = topInSuperAfter.y - topInSuperBefore.y
-			@layer.x -= xDiff
-			@layer.y -= yDiff
+		@_centerOrigin(event) if @centerOrigin

 	_pinch: (event) =>


Yes, upgrade header does need to be camel case.

--- src/trans-websocket-hybi10.coffee
+++ src/trans-websocket-hybi10.coffee
@@ -11,7 +11,7 @@
         key_accept = computeAcceptKey(@req.headers['sec-websocket-key'])
         blob = [
             'HTTP/1.1 101 Switching Protocols',
-            'Upgrade: websocket',
+            'Upgrade: WebSocket',
             'Connection: Upgrade',
             'Sec-WebSocket-Accept: ' + key_accept,
         ]


Restore previous definition of Attachment#toJSON

--- src/trix/models/attachment.coffee
+++ src/trix/models/attachment.coffee
@@ -27,7 +27,7 @@
     @attributes.filename.match(/\.(\w+)$/)?[1]

   toJSON: ->
-    {@id, @attributes}
+    @attributes

   toObject: ->
     {@id, @file, @attributes}


Refactored streaming

Refactored streaming

--- src/io.coffee
+++ src/io.coffee
@@ -104,12 +104,12 @@
     bindCallback promise, next

   _stream: (source) ->
-    {resolve, reject, promise} = When.defer()
     res = @_write source, '-'
-    res.promise.otherwise reject
-    res.stdout.on 'readable', ->
-      resolve res.stdout
-    promise
+    if res.stdout
+      When.resolve res.stdout
+    else
+      res.promise.then ->
+        throw new Error 'Can\'t read stdout'

   stream: (next) ->
     promise = if @_stdin
@@ -117,6 +117,7 @@
     else
       When(@_fileSource()).then (filename) =>
         @_stream filename
-      .ensure =>
-        @_cleanup '_tmpFilename'
+      .tap (stream) =>
+        stream.once 'close', =>
+          @_cleanup '_tmpFilename'
     bindCallback promise, next


Perform the "mark all entries read" and "toggle read/unread single entry" keyboard shortcuts only if there are actually entries loaded.

It's basically cleaner code, the actual behavior shouldn't change.

--- app/assets/javascripts/ng-services/ng-KeyboardShortcutsSvc.js.coffee
+++ app/assets/javascripts/ng-services/ng-KeyboardShortcutsSvc.js.coffee
@@ -15,6 +15,7 @@
     $(document).keypress (event)->
       # enter=13, spacebar=32, j=106, k=107, h=104, l=108, d=100, a=97, r=114
       if event.which in [13, 32, 104, 108, 106, 107, 100, 97, 114]
+
         # If a keyboard shortcut is used, stop highlighting entries on mousover until user actually moves the mouse.
         # This prevents accidental mouseover events when autoscrolling.
         $rootScope.mouseover_highlight_disabled = true
@@ -33,6 +34,17 @@

           # toggle open/close state of entry
           entrySvc.toggle_open_entry highlightedEntrySvc.get() if event.which == 32
+
+          # mark all entries as read
+          entrySvc.mark_all_read() if event.which == 97
+
+          # toggle read/unread state of currently highlighted entry
+          if event.which == 114
+            entry = highlightedEntrySvc.get()
+            if entry.read
+              entrySvc.unread_entry entry
+            else
+              entrySvc.read_entry entry

         # next link in sidebar
         highlightedSidebarLinkSvc.next() if event.which == 108
@@ -65,17 +77,6 @@
             readSvc.read_entries_page()
             menuCollapseSvc.close()

-        # mark all entries as read
-        entrySvc.mark_all_read() if event.which == 97
-
-        # toggle read/unread state of currently highlighted entry
-        if event.which == 114
-          entry = highlightedEntrySvc.get()
-          if entry.read
-            entrySvc.unread_entry entry
-          else
-            entrySvc.read_entry entry
-
         event.preventDefault()

   #---------------------------------------------


Give the component a valid id

Give the component a valid id

--- src/videojs.chromecastTech.coffee
+++ src/videojs.chromecastTech.coffee
@@ -19,9 +19,9 @@
     vjs.MediaTechController.call this, player, options, ready

     @el_ = videojs.Component::createEl("div",
-      id: "myId"
+      id: "#{@player_.id_}_chromecast_api"
       className: "vjs-tech"
-      innerHTML: "

Casting to device

"
+      innerHTML: "

Casting to device

"
     )

     vjs.insertFirst @el_, @player_.el()


pie chart angles now persist by ID and not by data

--- src/viz/helpers/shapes/arc.coffee
+++ src/viz/helpers/shapes/arc.coffee
@@ -1,6 +1,7 @@
 shapeStyle  = require "./style.coffee"
 largestRect = require "../../../geom/largestRect.coffee"
 path2poly   = require "../../../geom/path2poly.coffee"
+angles      = {start: {}, end: {}}

 module.exports = (vars, selection, enter, exit) ->

@@ -21,10 +22,10 @@
           angle: 0
         if rect[0]
           d.d3plus_label =
-            w:         rect[0].width
-            h:         rect[0].height
-            x:         rect[0].cx
-            y:         rect[0].cy
+            w: rect[0].width
+            h: rect[0].height
+            x: rect[0].cx
+            y: rect[0].cy
         else
           delete d.d3plus_label
     [d]
@@ -35,13 +36,13 @@
       .innerRadius 0
       .outerRadius (d) -> d.d3plus.r
       .startAngle (d) ->
-        d.d3plus.startAngleCurrent = 0 if d.d3plus.startAngleCurrent is undefined
-        d.d3plus.startAngleCurrent = d.d3plus.startAngle if isNaN(d.d3plus.startAngleCurrent)
-        d.d3plus.startAngleCurrent
+        angles.start[d.d3plus.id] = 0 if angles.start[d.d3plus.id] is undefined
+        angles.start[d.d3plus.id] = d.d3plus.startAngle if isNaN(angles.start[d.d3plus.id])
+        angles.start[d.d3plus.id]
       .endAngle (d) ->
-        d.d3plus.endAngleCurrent = 0 if d.d3plus.endAngleCurrent is undefined
-        d.d3plus.endAngleCurrent = d.d3plus.endAngle if isNaN(d.d3plus.endAngleCurrent)
-        d.d3plus.endAngleCurrent
+        angles.end[d.d3plus.id] = 0 if angles.end[d.d3plus.id] is undefined
+        angles.end[d.d3plus.id] = d.d3plus.endAngle if isNaN(angles.end[d.d3plus.id])
+        angles.end[d.d3plus.id]

     arcTween = (arcs, newAngle) ->
       arcs.attrTween "d", (d) ->
@@ -53,11 +54,11 @@
           s = 0
           e = 0

-        interpolateS = d3.interpolate(d.d3plus.startAngleCurrent, s)
-        interpolateE = d3.interpolate(d.d3plus.endAngleCurrent, e)
+        interpolateS = d3.interpolate(angles.start[d.d3plus.id], s)
+        interpolateE = d3.interpolate(angles.end[d.d3plus.id], e)
         (t) ->
-          d.d3plus.startAngleCurrent = interpolateS(t)
-          d.d3plus.endAngleCurrent   = interpolateE(t)
+          angles.start[d.d3plus.id] = interpolateS(t)
+          angles.end[d.d3plus.id] = interpolateE(t)
           newarc d

     enter.append("path")


fallback check for missing files

fallback check for missing files

dot files are not examined by OutputFileFinder, so do an extra check to
make sure those exist

also check for any relative paths in the resources

--- services/clsi/app/coffee/ResourceStateManager.coffee
+++ services/clsi/app/coffee/ResourceStateManager.coffee
@@ -4,6 +4,7 @@
 settings = require("settings-sharelatex")
 Errors = require "./Errors"
 SafeReader = require "./SafeReader"
+async = require "async"

 module.exports = ResourceStateManager =

@@ -50,14 +51,31 @@
 				resources = ({path: path} for path in resourceList)
 				callback(null, resources)

-	checkResourceFiles: (resources, allFiles, directory, callback = (error) ->) ->
+	checkResourceFiles: (resources, allFiles, basePath, callback = (error) ->) ->
+		# check the paths are all relative to current directory
+		for file in resources or []
+			for dir in file?.path?.split('/')
+				if dir == '..'
+					return callback new Error("relative path in resource file list")
 		# check if any of the input files are not present in list of files
 		seenFile = {}
 		for file in allFiles
 			seenFile[file] = true
-		missingFiles = (resource.path for resource in resources when not seenFile[resource.path])
-		if missingFiles.length > 0
-			logger.err missingFiles:missingFiles, dir:directory, allFiles:allFiles, resources:resources, "missing input files for project"
-			return callback new Errors.FilesOutOfSyncError("resource files missing in incremental update")
+		missingFileCandidates = (resource.path for resource in resources when not seenFile[resource.path])
+		# now check if they are really missing
+		ResourceStateManager._checkMissingFiles missingFileCandidates, basePath, (missingFiles) ->
+			if missingFiles?.length > 0
+				logger.err missingFiles:missingFiles, basePath:basePath, allFiles:allFiles, resources:resources, "missing input files for project"
+				return callback new Errors.FilesOutOfSyncError("resource files missing in incremental update")
+			else
+				callback()
+
+	_checkMissingFiles: (missingFileCandidates, basePath, callback = (missingFiles) ->) ->
+		if missingFileCandidates.length > 0
+			fileDoesNotExist = (file, cb) ->
+				fs.stat Path.join(basePath, file), (err) ->
+					logger.log file:file, err:err, result: err?, "stating potential missing file"
+					cb(err?)
+			async.filterSeries missingFileCandidates, fileDoesNotExist, callback
 		else
-			callback()
+			callback([])


increase profiling cutoff to 1s

increase profiling cutoff to 1s

--- services/document-updater/app/coffee/Profiler.coffee
+++ services/document-updater/app/coffee/Profiler.coffee
@@ -5,7 +5,7 @@
 	return Math.floor(((ta[0]-tb[0])*1e9 + (ta[1]-tb[1]))*1e-6)

 module.exports = class Profiler
-	LOG_CUTOFF_TIME: 100
+	LOG_CUTOFF_TIME: 1000

 	constructor: (@name, @args) ->
 		@t0 = @t = process.hrtime()


log profilers start and end times for reference

--- services/document-updater/app/coffee/Profiler.coffee
+++ services/document-updater/app/coffee/Profiler.coffee
@@ -9,6 +9,7 @@

 	constructor: (@name, @args) ->
 		@t0 = @t = process.hrtime()
+		@start = new Date()
 		@updateTimes = []

 	log: (label) ->
@@ -25,4 +26,6 @@
 		for k,v of @args
 			args[k] = v
 		args.updateTimes = @updateTimes
+		args.start = @start
+		args.end = new Date()
 		logger.log args, @name


code formatting

code formatting

--- lib/submachine.coffee
+++ lib/submachine.coffee
@@ -45,9 +45,13 @@
     @callbacks[ @state ] ?= {}
     @callbacks[ state ]  ?= {}

-    @callbacks[ @state ].onLeave.apply( @, args ) if @state? and @callbacks[ @state ].onLeave?
+    if @state? and @callbacks[ @state ].onLeave?
+      @callbacks[ @state ].onLeave.apply( @, args )
+
     @state = state
-    @callbacks[ @state ].onEnter.apply( @, args ) if @callbacks[ @state ].onEnter?
+
+    if @callbacks[ @state ].onEnter?
+      @callbacks[ @state ].onEnter.apply( @, args )

   onEnter: ( state, cbk ) ->
     @callbacks ?= {}


fixed the htmleditor spec (missed previously?)

--- spec/javascripts/mercury/modals/htmleditor_spec.js.coffee
+++ spec/javascripts/mercury/modals/htmleditor_spec.js.coffee
@@ -10,7 +10,7 @@
       hide: ->
     Mercury.region =
       content: -> 'html \ncontent'
-    Mercury.modalHandlers.htmleditor.call(@modal)
+    Mercury.modalHandlers.htmlEditor.call(@modal)

   describe "loading", ->


hide the awesome arrowbox with cancel

hide the awesome arrowbox with cancel

--- lib/bottom-dock/results-pane.coffee
+++ lib/bottom-dock/results-pane.coffee
@@ -91,6 +91,7 @@
         @tables = []
         @emitter = new Emitter()
         @subscriptions = new CompositeDisposable()
+        @subscriptions.add atom.commands.add 'body', 'core:cancel': => @reset()

     updateNotComplete: (newBatch, result, queryNumber, bufferRange) ->
         if newBatch


Added .noselect to splash

Added .noselect to splash

--- app/src/Xjs/XTechneStartup.coffee
+++ app/src/Xjs/XTechneStartup.coffee
@@ -23,7 +23,7 @@
     displaySplash: () ->
         @panel = document.createElement 'div'
         @panel.classList.add 'xtechneStartupContainer'
-        content = 'OmphalOS'
+        content = 'OmphalOS'
         @panel.insertAdjacentHTML 'beforeend', content
         @osName = document.getElementById 'XTechneStartupOsName'
         @displaySurface.appendChild @panel


Fixes issue on startup where config isn't defaulted properly.

--- lib/event-watch-view.coffee
+++ lib/event-watch-view.coffee
@@ -17,6 +17,8 @@
     @timer = null
     @visible = true
     @parsedSchedules = {}
+    @subscriptions = []
+    @schedules = {}

   # Public: Attach view element to status bar and do initial setup.
   attach: ->


Second stab at API

Second stab at API

--- lib/ghc-modi-process.coffee
+++ lib/ghc-modi-process.coffee
@@ -1,22 +1,53 @@
 {Range} = require('atom')
 Temp = require('temp')
 FS = require('fs')
+CP = require('child_process')

 module.exports =
 class GhcModiProcess
   constructor: ->
-    @path = atom.config.get('haskell-ghc-mod.ghcModiPath')
-    @process = require('child_process').spawn(@path,['-b\r'])
+    @modiPath = atom.config.get('haskell-ghc-mod.ghcModiPath')
+    @modPath = atom.config.get('haskell-ghc-mod.ghcModPath')
+    @process = CP.spawn(@modiPath,['-b\r'])
+    @services=atom.services.provide "haskell-ghc-mod", "0.1.0",
+      type: @getType
+      info: @getInfo
+      check: @doCheck
+      list: @runList
+      lang: @runLang
+      flag: @runFlag
+      browse: @runBrowse
+

   # Tear down any state and detach
   destroy: ->
+    @services.dispose()
     @process.stdin.end()

   runCmd: (command, callback) ->
     @process.stdout.once 'data', callback
     @process.stdin.write(command)

-  getType: (text, crange, callback) ->
+  runModCmd: (args,callback) =>
+    CP.execFile @modPath, args, {}, callback
+
+  runList: (callback) =>
+    @runModCmd ['list'], (error,result) ->
+      callback result if not error
+
+  runLang: (callback) =>
+    @runModCmd ['lang'], (error,result) ->
+      callback result if not error
+
+  runFlag: (callback) =>
+    @runModCmd ['flag'], (error,result) ->
+      callback result if not error
+
+  runBrowse: (module,callback) =>
+    @runModCmd ['browse','-d',module], (error,result) ->
+      callback result if not error
+
+  getType: (text, crange, callback) =>
     Temp.open
       prefix:'haskell-ghc-mod',
       suffix:'.hs',
@@ -45,7 +76,7 @@
           range=crange unless range
           callback range,type.replace(/\r/g,'\n'),crange

-  getInfo: (text,symbol,callback) ->
+  getInfo: (text,symbol,callback) =>
     Temp.open
       prefix:'haskell-ghc-mod',
       suffix:'.hs',
@@ -61,7 +92,7 @@
             return true unless line=="OK" || line==""
           callback lines.join('\n').replace(/\r/g,'\n')

-  doCheck: (text, callback) ->
+  doCheck: (text, callback) =>
     Temp.open
       prefix:'haskell-ghc-mod',
       suffix:'.hs',


Don't forget to remove @_cursors

Don't forget to remove @_cursors

--- lib/y-richtext.coffee
+++ lib/y-richtext.coffee
@@ -57,6 +57,7 @@

     delete @_characters
     delete @_selections
+    delete @_cursors

   # insert our own cursor in the cursors list
   # @param position [Integer] the position where to insert it


:racehorse: Efficient updates

:racehorse: Efficient updates

--- lib/message-registry.coffee
+++ lib/message-registry.coffee
@@ -33,12 +33,26 @@
     return unless @shouldUpdatePublic
     if @updated
       @updated = false
+
       publicMessages = []
+      added = []
+      removed = []
+
       @linterResponses.forEach (messages) -> publicMessages = publicMessages.concat(messages)
       @editorMessages.forEach (linters) -> linters.forEach (messages) ->
         publicMessages = publicMessages.concat(messages)
+
+      currentKeys = publicMessages.map (i) -> i.key
+      lastKeys = publicMessages.map (i) -> i.key
+
+      publicMessages.forEach (i) ->
+        added.push(i) if lastKeys.indexOf(i) is -1
+      @publicMessages.forEach (i) ->
+        removed.push(i) if currentKeys.indexOf(i) is -1
+
       @publicMessages = publicMessages
-      @emitter.emit 'did-update-messages', @publicMessages
+      @emitter.emit 'did-update-messages', {added, removed}
+
     requestAnimationFrame => @updatePublic()

   onDidUpdateMessages: (callback) ->


Bug fix

Bug fix

--- lib/model_extensions.coffee
+++ lib/model_extensions.coffee
@@ -110,7 +110,7 @@
       model = new model_type(data)
       model.save {}, (err) ->
         return callback(err) if err
-        cache.add(model.id, model) if cache = model_type.cache
+        cache.set(model.id, model) if cache = model_type.cache
         callback(null, model)

   model_type.findOneNearestDate = (date, options, query, callback) ->


Improve initialization

Improve initialization

--- app/templates/src/template.coffee
+++ app/templates/src/template.coffee
@@ -17,9 +17,13 @@
 <% if (needStorage) { %>class <%= robotClassName %>Robot
   constructor: (@robot) -><% if (needEnvVariable) { %>
     @config = process.env.HUBOT_<%= scriptNameUppercased %>_SETTING or 'whatever the default value should be'<% } %>
-    <%= scriptNameCamelized %> = @robot.brain.get '<%= scriptNameCamelized %>'
-    @<%= scriptNameCamelized %> = <%= scriptNameCamelized %> or []
-    @robot.brain.set '<%= scriptNameCamelized %>', @<%= scriptNameCamelized %>
+    initialized = false
+    robot.brain.on 'loaded', =>
+      if not initialized
+        initialized = true
+        <%= scriptNameCamelized %> = @robot.brain.get '<%= scriptNameCamelized %>'
+        @<%= scriptNameCamelized %> = <%= scriptNameCamelized %> or []
+        @robot.brain.set '<%= scriptNameCamelized %>', @<%= scriptNameCamelized %>
   add: (item) ->
     @<%= scriptNameCamelized %>.push item
     @robot.brain.set '<%= scriptNameCamelized %>', @<%= scriptNameCamelized %>


not used

not used

--- lib/register-manager.coffee
+++ lib/register-manager.coffee
@@ -10,8 +10,6 @@
 # been set.
 module.exports =
 class RegisterManager
-  name = null
-
   constructor: (@vimState) ->
     {@editor, @globalVimState} = @vimState


:memo: Convert Config docs

:memo: Convert Config docs

--- src/config.coffee
+++ src/config.coffee
@@ -15,9 +15,9 @@
 # * Create your own root keypath using your package's name.
 # * Don't depend on (or write to) configuration keys outside of your keypath.
 #
-# ## Example
-#
-# ```coffeescript
+# ## Examples
+#
+# ```coffee
 # atom.config.set('my-package.key', 'value')
 # atom.config.observe 'my-package.key', ->
 #   console.log 'My configuration changed:', atom.config.get('my-package.key')
@@ -88,17 +88,17 @@
     _.extend hash, defaults
     @emit 'updated'

-  # Public: Get the {String} path to the config file being used.
+  # Extended: Get the {String} path to the config file being used.
   getUserConfigPath: ->
     @configFilePath

-  # Public: Returns a new {Object} containing all of settings and defaults.
+  # Extended: Returns a new {Object} containing all of settings and defaults.
   getSettings: ->
     _.deepExtend(@settings, @defaultSettings)

-  # Public: Retrieves the setting for the given key.
-  #
-  # keyPath - The {String} name of the key to retrieve.
+  # Essential: Retrieves the setting for the given key.
+  #
+  # * `keyPath` The {String} name of the key to retrieve.
   #
   # Returns the value from Atom's default settings, the user's configuration
   # file, or `null` if the key doesn't exist in either.
@@ -117,19 +117,19 @@

     value

-  # Public: Retrieves the setting for the given key as an integer.
-  #
-  # keyPath - The {String} name of the key to retrieve
+  # Extended: Retrieves the setting for the given key as an integer.
+  #
+  # * `keyPath` The {String} name of the key to retrieve
   #
   # Returns the value from Atom's default settings, the user's configuration
   # file, or `NaN` if the key doesn't exist in either.
   getInt: (keyPath) ->
     parseInt(@get(keyPath))

-  # Public: Retrieves the setting for the given key as a positive integer.
-  #
-  # keyPath - The {String} name of the key to retrieve
-  # defaultValue - The integer {Number} to fall back to if the value isn't
+  # Extended: Retrieves the setting for the given key as a positive integer.
+  #
+  # * `keyPath` The {String} name of the key to retrieve
+  # * `defaultValue` The integer {Number} to fall back to if the value isn't
   #                positive, defaults to 0.
   #
   # Returns the value from Atom's default settings, the user's configuration
@@ -137,12 +137,12 @@
   getPositiveInt: (keyPath, defaultValue=0) ->
     Math.max(@getInt(keyPath), 0) or defaultValue

-  # Public: Sets the value for a configuration setting.
+  # Essential: Sets the value for a configuration setting.
   #
   # This value is stored in Atom's internal configuration file.
   #
-  # keyPath - The {String} name of the key.
-  # value - The value of the setting.
+  # * `keyPath` The {String} name of the key.
+  # * `value` The value of the setting.
   #
   # Returns the `value`.
   set: (keyPath, value) ->
@@ -153,47 +153,47 @@
       @update()
     value

-  # Public: Toggle the value at the key path.
+  # Extended: Toggle the value at the key path.
   #
   # The new value will be `true` if the value is currently falsy and will be
   # `false` if the value is currently truthy.
   #
-  # keyPath - The {String} name of the key.
+  # * `keyPath` The {String} name of the key.
   #
   # Returns the new value.
   toggle: (keyPath) ->
     @set(keyPath, !@get(keyPath))

-  # Public: Restore the key path to its default value.
-  #
-  # keyPath - The {String} name of the key.
+  # Extended: Restore the key path to its default value.
+  #
+  # * `keyPath` The {String} name of the key.
   #
   # Returns the new value.
   restoreDefault: (keyPath) ->
     @set(keyPath, _.valueForKeyPath(@defaultSettings, keyPath))

-  # Public: Get the default value of the key path.
-  #
-  # keyPath - The {String} name of the key.
+  # Extended: Get the default value of the key path.
+  #
+  # * `keyPath` The {String} name of the key.
   #
   # Returns the default value.
   getDefault: (keyPath) ->
     defaultValue = _.valueForKeyPath(@defaultSettings, keyPath)
     _.deepClone(defaultValue)

-  # Public: Is the key path value its default value?
-  #
-  # keyPath - The {String} name of the key.
+  # Extended: Is the key path value its default value?
+  #
+  # * `keyPath` The {String} name of the key.
   #
   # Returns a {Boolean}, `true` if the current value is the default, `false`
   # otherwise.
   isDefault: (keyPath) ->
     not _.valueForKeyPath(@settings, keyPath)?

-  # Public: Push the value to the array at the key path.
-  #
-  # keyPath - The {String} key path.
-  # value - The value to push to the array.
+  # Extended: Push the value to the array at the key path.
+  #
+  # * `keyPath` The {String} key path.
+  # * `value` The value to push to the array.
   #
   # Returns the new array length {Number} of the setting.
   pushAtKeyPath: (keyPath, value) ->
@@ -202,10 +202,10 @@
     @set(keyPath, arrayValue)
     result

-  # Public: Add the value to the beginning of the array at the key path.
-  #
-  # keyPath - The {String} key path.
-  # value - The value to shift onto the array.
+  # Extended: Add the value to the beginning of the array at the key path.
+  #
+  # * `keyPath` The {String} key path.
+  # * `value` The value to shift onto the array.
   #
   # Returns the new array length {Number} of the setting.
   unshiftAtKeyPath: (keyPath, value) ->
@@ -216,8 +216,8 @@

   # Public: Remove the value from the array at the key path.
   #
-  # keyPath - The {String} key path.
-  # value - The value to remove from the array.
+  # * `keyPath` The {String} key path.
+  # * `value` The value to remove from the array.
   #
   # Returns the new array value of the setting.
   removeAtKeyPath: (keyPath, value) ->
@@ -226,20 +226,17 @@
     @set(keyPath, arrayValue)
     result

-  # Public: Establishes an event listener for a given key.
-  #
-  # `callback` is fired whenever the value of the key is changed and will
-  #  be fired immediately unless the `callNow` option is `false`.
-  #
-  # keyPath - The {String} name of the key to observe
-  # options - An optional {Object} containing the `callNow` key.
-  # callback - The {Function} to call when the value of the key changes.
-  #            The first argument will be the new value of the key and the
-  #            second argument will be an {Object} with a `previous` property
-  #            that is the prior value of the key.
+  # Essential: Add a listener for changes to a given key path.
+  #
+  # * `keyPath` The {String} name of the key to observe
+  # * `options` An optional {Object} containing the `callNow` key.
+  # * `callback` The {Function} to call when the value of the key changes.
+  #              The first argument will be the new value of the key and the
+  #              second argument will be an {Object} with a `previous` property
+  #              that is the prior value of the key.
   #
   # Returns an {Object} with the following keys:
-  #  :off - A {Function} that unobserves the `keyPath` when called.
+  #  * `off` A {Function} that unobserves the `keyPath` when called.
   observe: (keyPath, options={}, callback) ->
     if _.isFunction(options)
       callback = options
@@ -259,9 +256,9 @@
     callback(value) if options.callNow ? true
     subscription

-  # Public: Unobserve all callbacks on a given key.
-  #
-  # keyPath - The {String} name of the key to unobserve.
+  # Unobserve all callbacks on a given key.
+  #
+  # * `keyPath` The {String} name of the key to unobserve.
   unobserve: (keyPath) ->
     @off("updated.#{keyPath.replace(/\./, '-')}")


Remove unnecessary fat arrow

Remove unnecessary fat arrow

--- src/config.coffee
+++ src/config.coffee
@@ -699,7 +699,7 @@
         @endTransaction()
         fn(args...)
       result = callback()
-      new Promise (resolve, reject) =>
+      new Promise (resolve, reject) ->
         result.then(endTransaction(resolve)).catch(endTransaction(reject))
     catch error
       @endTransaction()


Move method to be with similar methods in the docs.

--- src/cursor.coffee
+++ src/cursor.coffee
@@ -420,38 +420,6 @@
   Section: Local Positions and Ranges
   ###

-  # Public: Retrieves the buffer position of where the current word starts.
-  #
-  # * `options` (optional) An {Object} with the following keys:
-  #   * `wordRegex` A {RegExp} indicating what constitutes a "word"
-  #     (default: {::wordRegExp}).
-  #   * `includeNonWordCharacters` A {Boolean} indicating whether to include
-  #     non-word characters in the default word regex.
-  #     Has no effect if wordRegex is set.
-  #   * `allowPrevious` A {Boolean} indicating whether the beginning of the
-  #     previous word can be returned.
-  #
-  # Returns a {Range}.
-  getBeginningOfCurrentWordBufferPosition: (options = {}) ->
-    allowPrevious = options.allowPrevious ? true
-    currentBufferPosition = @getBufferPosition()
-    previousNonBlankRow = @editor.buffer.previousNonBlankRow(currentBufferPosition.row) ? 0
-    scanRange = [[previousNonBlankRow, 0], currentBufferPosition]
-
-    beginningOfWordPosition = null
-    @editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, stop}) ->
-      if range.end.isGreaterThanOrEqual(currentBufferPosition) or allowPrevious
-        beginningOfWordPosition = range.start
-      if not beginningOfWordPosition?.isEqual(currentBufferPosition)
-        stop()
-
-    if beginningOfWordPosition?
-      beginningOfWordPosition
-    else if allowPrevious
-      new Point(0, 0)
-    else
-      currentBufferPosition
-
   # Public: Retrieves buffer position of previous word boundary. It might be on
   # the current word, or the previous word.
   #
@@ -503,6 +471,38 @@

     endOfWordPosition or currentBufferPosition

+  # Public: Retrieves the buffer position of where the current word starts.
+  #
+  # * `options` (optional) An {Object} with the following keys:
+  #   * `wordRegex` A {RegExp} indicating what constitutes a "word"
+  #     (default: {::wordRegExp}).
+  #   * `includeNonWordCharacters` A {Boolean} indicating whether to include
+  #     non-word characters in the default word regex.
+  #     Has no effect if wordRegex is set.
+  #   * `allowPrevious` A {Boolean} indicating whether the beginning of the
+  #     previous word can be returned.
+  #
+  # Returns a {Range}.
+  getBeginningOfCurrentWordBufferPosition: (options = {}) ->
+    allowPrevious = options.allowPrevious ? true
+    currentBufferPosition = @getBufferPosition()
+    previousNonBlankRow = @editor.buffer.previousNonBlankRow(currentBufferPosition.row) ? 0
+    scanRange = [[previousNonBlankRow, 0], currentBufferPosition]
+
+    beginningOfWordPosition = null
+    @editor.backwardsScanInBufferRange (options.wordRegex ? @wordRegExp(options)), scanRange, ({range, stop}) ->
+      if range.end.isGreaterThanOrEqual(currentBufferPosition) or allowPrevious
+        beginningOfWordPosition = range.start
+      if not beginningOfWordPosition?.isEqual(currentBufferPosition)
+        stop()
+
+    if beginningOfWordPosition?
+      beginningOfWordPosition
+    else if allowPrevious
+      new Point(0, 0)
+    else
+      currentBufferPosition
+
   # Public: Retrieves the buffer position of where the current word ends.
   #
   # * `options` (optional) {Object} with the following keys:


Fixed detection of orientation support

Fixed detection of orientation support

Hi,

CoffeeScript's ``in`` keyword generates an __indexOf() search for "orientationchange" in the window object. I assume this is not what you intended - therefore I changed it to

```coffeescript
_supports_orientation = "onorientationchange" of window
```

Which translates to (JavaScript):

```javascript
var _supports_orientation;
_supports_orientation = "onorientationchange" in window;
```

--- src/device.coffee
+++ src/device.coffee
@@ -157,7 +157,7 @@

 # Detect whether device supports orientationchange event,
 # otherwise fall back to the resize event.
-_supports_orientation = "onorientationchange" in window
+_supports_orientation = "onorientationchange" of window
 _orientation_event = if _supports_orientation then "orientationchange" else "resize"

 # Listen for changes in orientation.


Reorganize Tab Behavior section

Reorganize Tab Behavior section

--- src/editor.coffee
+++ src/editor.coffee
@@ -1056,7 +1056,27 @@
   Section: Tab Behavior
   ###

-  # Public: Determine if the buffer uses hard or soft tabs.
+  # Essential: Returns a {Boolean} indicating whether softTabs are enabled for this
+  # editor.
+  getSoftTabs: -> @softTabs
+
+  # Essential: Enable or disable soft tabs for this editor.
+  #
+  # * `softTabs` A {Boolean}
+  setSoftTabs: (@softTabs) -> @softTabs
+
+  # Essential: Toggle soft tabs for this editor
+  toggleSoftTabs: -> @setSoftTabs(not @getSoftTabs())
+
+  # Essential: Get the on-screen length of tab characters.
+  #
+  # Returns a {Number}.
+  getTabLength: -> @displayBuffer.getTabLength()
+
+  # Essential: Set the on-screen length of tab characters.
+  setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength)
+
+  # Extended: Determine if the buffer uses hard or soft tabs.
   #
   # Returns `true` if the first non-comment line with leading whitespace starts
   # with a space character. Returns `false` if it starts with a hard tab (`\t`).
@@ -1073,33 +1093,13 @@

     undefined

-  # Public: Returns a {Boolean} indicating whether softTabs are enabled for this
-  # editor.
-  getSoftTabs: -> @softTabs
-
-  # Public: Enable or disable soft tabs for this editor.
-  #
-  # * `softTabs` A {Boolean}
-  setSoftTabs: (@softTabs) -> @softTabs
-
-  # Public: Toggle soft tabs for this editor
-  toggleSoftTabs: -> @setSoftTabs(not @getSoftTabs())
-
-  # Public: Get the text representing a single level of indent.
+  # Extended: Get the text representing a single level of indent.
   #
   # If soft tabs are enabled, the text is composed of N spaces, where N is the
   # tab length. Otherwise the text is a tab character (`\t`).
   #
   # Returns a {String}.
   getTabText: -> @buildIndentString(1)
-
-  # Public: Get the on-screen length of tab characters.
-  #
-  # Returns a {Number}.
-  getTabLength: -> @displayBuffer.getTabLength()
-
-  # Public: Set the on-screen length of tab characters.
-  setTabLength: (tabLength) -> @displayBuffer.setTabLength(tabLength)

   # If soft tabs are enabled, convert all hard tabs to soft tabs in the given
   # {Range}.


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/engine.coffee
+++ src/engine.coffee
@@ -205,7 +205,7 @@
       @methodOf Engine#
       @name start
       ###
-      start: () ->
+      start: ->
         unless running
           running = true
           window.requestAnimationFrame(animLoop)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/engine.coffee
+++ src/engine.coffee
@@ -254,9 +254,6 @@
     }

     self.include Bindable
-
-    modules = Engine.defaultModules.concat(I.includedModules)
-    modules = modules.without([].concat(I.excludedModules))

     Engine.defaultModules.each (moduleName) ->
       fullModuleName = "Engine.#{moduleName}"


Corrected argument passed to error super constructors

--- src/errors.coffee
+++ src/errors.coffee
@@ -29,23 +29,23 @@

 class ApiError extends CogsError
   constructor: (message, cause, statusCode, details) ->
-    super message, cause, statuscode, details
+    super message, cause, statusCode, details

 class InfoError extends CogsError
   constructor: (message, cause, statusCode, details) ->
-    super message, cause, statuscode, details
+    super message, cause, statusCode, details

 class ToolsError extends CogsError
   constructor: (message, cause, statusCode, details) ->
-    super message, cause, statuscode, details
+    super message, cause, statusCode, details

 class PubSubError extends CogsError
   constructor: (message, cause, statusCode, details) ->
-    super message, cause, statuscode, details
+    super message, cause, statusCode, details

 class PubSubFailureResponse extends PubSubError
   constructor: (message, cause, statusCode, details, @record) ->
-    super message, cause, statuscode, details
+    super message, cause, statusCode, details

 module.exports =
   ApiError: ApiError


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/follow.coffee
+++ src/follow.coffee
@@ -63,8 +63,8 @@

   </pre></code>

-  @name clamp
-  @methodOf Clampable#
+  @name follow
+  @methodOf Follow#
   @param {Object} data
   ###
   follow: (obj) ->


follow docs

follow docs

--- src/follow.coffee
+++ src/follow.coffee
@@ -17,6 +17,7 @@
   height: 10
   velocity: Point(0, 0)

+# Make an enemy follow the player
 enemy.follow(player)

 # now the enemy's velocity will point toward the player


follow docs

follow docs

--- src/follow.coffee
+++ src/follow.coffee
@@ -17,7 +17,7 @@
   height: 10
   velocity: Point(0, 0)

-enemy.
+enemy.follow

 player.update()
 </pre></code>


Add fusion_tables_query method.

Add fusion_tables_query method.

--- src/js/collection-editor.coffee
+++ src/js/collection-editor.coffee
@@ -1,3 +1,5 @@
+FUSION_TABLES_URI = 'https://www.googleapis.com/fusiontables/v1'
+
 google_oauth_parameters_for_fusion_tables =
   response_type: 'token'
   client_id: '891199912324.apps.googleusercontent.com'
@@ -52,6 +54,16 @@
   form.append $('<label>').attr('for',$(property).attr('name')).append($(property).attr('label') + ':')
   form.append build_input_for_property property

+fusion_tables_query = (query) ->
+  $.ajax "#{FUSION_TABLES_URI}/query?sql=#{query}&access_token=#{get_cookie 'access_token'}",
+    type: 'GET'
+    dataType: 'json'
+    crossDomain: true
+    error: (jqXHR, textStatus, errorThrown) ->
+      console.log "AJAX Error: #{textStatus}"
+    success: (data) ->
+      console.log data
+
 save_collection_form = ->
   collection = $('#collection_select').val()
   localStorage[collection] = true
@@ -97,7 +109,7 @@

   # test table access
   if get_cookie 'access_token'
-    $.ajax "https://www.googleapis.com/fusiontables/v1/tables/#{$(collection).attr('class')}?access_token=#{get_cookie 'access_token'}",
+    $.ajax "#{FUSION_TABLES_URI}/tables/#{$(collection).attr('class')}?access_token=#{get_cookie 'access_token'}",
       type: 'GET'
       dataType: 'json'
       crossDomain: true
@@ -107,6 +119,7 @@
         disable_collection_form()
       success: (data) ->
         console.log data
+        fusion_tables_query("SELECT ROWID FROM #{$(collection).attr('class')}")

   $('.container').append form
   set_author_name()


more hooks

more hooks

--- src/hinoki.coffee
+++ src/hinoki.coffee
@@ -106,6 +106,7 @@
                 if hasErrorOccured
                     return
                 try
+                    container.hooks?.factory? newChain, result.factory, arguments
                     instance = result.factory.apply null, arguments
                 catch err
                     hasErrorOccured = true
@@ -113,11 +114,15 @@
                     return

                 unless q.isPromise instance
+                    container.hooks?.instance? newChain, instance
                     container.instances[id] = instance
                     resolved[id] = instance
                     return

+                container.hooks?.promise? newChain, instance
+
                 onSuccess = (value) ->
+                    container.hooks?.resolution? newChain, value
                     result.containers[0].instances[id] = value
                     resolved[id] = value
                     maybeResolved()


make array from arguments

make array from arguments

--- src/hinoki.coffee
+++ src/hinoki.coffee
@@ -106,7 +106,7 @@
                 if hasErrorOccured
                     return
                 try
-                    container.hooks?.factory? newChain, result.factory, arguments
+                    container.hooks?.factory? newChain, result.factory, Array.prototype.slice.call arguments
                     instance = result.factory.apply null, arguments
                 catch err
                     hasErrorOccured = true


removed unnecessary boot and fixed buggy stop method

--- modules/gui/platforms/node/lib/pages/PageContext.coffee
+++ modules/gui/platforms/node/lib/pages/PageContext.coffee
@@ -37,13 +37,7 @@
         ##
         isPageContext: ()->
             return true
-
-        ##
-        ##
-        ##
-        boot: (done)->
-
-            super done
+

         ##
         ##
@@ -73,8 +67,13 @@
         ##
         ##
         ##
-        stop: ()->
-            @_watcher?.close()
+        stop: (done)->
+            try
+                @_watcher?.close()
+            catch e
+                return done e
+
+            super done


Only terminate the task when there are no other pending callbacks

--- lib/spell-check-task.coffee
+++ lib/spell-check-task.coffee
@@ -12,8 +12,10 @@

   terminate: ->
     delete @constructor.callbacksById[@id]
-    @constructor.task?.terminate()
-    @constructor.task = null
+
+    if Object.keys(@constructor.callbacksById).length is 0
+      @constructor.task?.terminate()
+      @constructor.task = null

   start: (text) ->
     @constructor.task ?= new Task(require.resolve('./spell-check-handler'))


Rename destroyTask to terminateTask

Rename destroyTask to terminateTask

--- lib/spell-check-view.coffee
+++ lib/spell-check-view.coffee
@@ -14,7 +14,7 @@
     else
       @activeViews = 1

-  @destroyTask: ->
+  @terminateTask: ->
     @activeViews--
     if @activeViews is 0
       @task?.terminate()
@@ -36,7 +36,7 @@

   beforeRemove: ->
     @unsubscribeFromBuffer()
-    @constructor.destroyTask()
+    @constructor.terminateTask()

   unsubscribeFromBuffer: ->
     @destroyViews()


Unsubscribe correctly from buffers in packages

Previously namespaces were used to off() when a buffer was
unsubscribed from which affected other views in the same
package also listening to the current buffer.

Now event namespaces are no longer used and instead off() is
called with the callbacks originally registered for the given
event name.

--- lib/spell-check-view.coffee
+++ lib/spell-check-view.coffee
@@ -11,27 +11,30 @@
   views: []

   initialize: (@editor) ->
-    @subscribe @editor, 'editor:path-changed', => @subscribeToBuffer()
-    @subscribe @editor, 'editor:grammar-changed', => @subscribeToBuffer()
-    @observeConfig 'editor.fontSize', => @subscribeToBuffer()
-    @observeConfig 'spell-check.grammars', => @subscribeToBuffer()
+    @subscribe @editor, 'editor:path-changed', @subscribeToBuffer
+    @subscribe @editor, 'editor:grammar-changed', @subscribeToBuffer
+    @observeConfig 'editor.fontSize', @subscribeToBuffer
+    @observeConfig 'spell-check.grammars', @subscribeToBuffer

     @subscribeToBuffer()
+
+  beforeRemove: ->
+    @unsubscribeFromBuffer()

   unsubscribeFromBuffer: ->
     @destroyViews()
     @task?.abort()

     if @buffer?
-      @buffer.off '.spell-check'
+      @buffer.off 'contents-modified', @updateMisspellings
       @buffer = null

-  subscribeToBuffer: ->
+  subscribeToBuffer: =>
     @unsubscribeFromBuffer()

     if @spellCheckCurrentGrammar()
       @buffer = @editor.getBuffer()
-      @buffer.on 'contents-modified.spell-check', => @updateMisspellings()
+      @buffer.on 'contents-modified', @updateMisspellings
       @updateMisspellings()

   spellCheckCurrentGrammar: ->
@@ -49,11 +52,7 @@
       @views.push(view)
       @append(view)

-  updateMisspellings: ->
-    unless @editor.activeEditSession?
-      @unsubscribeFromBuffer()
-      return
-
+  updateMisspellings: =>
     @task?.abort()

     callback = (misspellings) =>


dropped the word 'time' from the English versions of time labels

--- nls/en-us/strings.coffee
+++ nls/en-us/strings.coffee
@@ -7,8 +7,8 @@
     ferry: 'Ferries'
   from: 'Origin'
   to: 'Destination'
-  departureTime: 'Departure time'
-  arrivalTime: 'Arrival time'
+  departureTime: 'Departure'
+  arrivalTime: 'Arrival'
   findRoutes: 'Find routes'
   findingRoutes: 'Finding routes...'
   competitionEntry: 'Vote for us on Apps4Finland!'


Hide resolved text when collapsed

Hide resolved text when collapsed

--- resources/assets/coffee/react/beatmap-discussions/discussion.coffee
+++ resources/assets/coffee/react/beatmap-discussions/discussion.coffee
@@ -77,7 +77,7 @@

         if @props.discussion.resolved
           div
-            className: "#{bn}__resolved"
+            className: "#{bn}__resolved #{'hidden' if @state.collapsed}"
             Lang.get 'beatmaps.discussions.resolved'


Fix authentication providers iteration.

Fix authentication providers iteration.

--- src/xo.coffee
+++ src/xo.coffee
@@ -29,7 +29,7 @@
 } = require './utils'
 {$MappedCollection} = require './MappedCollection'

-{default: {Set}} = require 'babel-runtime/core-js'
+{Set, $for: {getIterator}} = (require 'babel-runtime/core-js').default

 #=====================================================================
 # Models and collections.
@@ -562,7 +562,7 @@
     else if credentials.username?
       credentials.email = credentials.username

-    iterator = @_authenticationProviders[Symbol.iterator]()
+    iterator = getIterator(@_authenticationProviders)

     while not (current = iterator.next()).done
       try


invitations: disable token requirements in localhost, koding.com

--- client/Login/AppController.coffee
+++ client/Login/AppController.coffee
@@ -20,6 +20,9 @@

   handleRestriction = (handler) ->
     ({params: {token : token }})->
+      for url in ['localhost', 'https:\/\/koding.com'] when (new RegExp url).test window.location
+        return do handler()
+
       return handleFailureOfRestriction()  unless token

       KD.remote.api.JInvitation.byCode token, (err, invite)->


KiteCache::unset: not only remove exact query but also removes all alternatives

--- client/Main/kite/kitecache.coffee
+++ client/Main/kite/kitecache.coffee
@@ -58,7 +58,40 @@
     if typeof query is 'object'
       query = @generateQueryString query

-    delete storage[signed query]
+    ltr = ""
+    rtl = ""
+
+    # It may look crpytic but what it does is very simple;
+    # It generates all possible version of given Kite query and removes
+    # them from cache if they are exists. ~ GG
+    #
+    # For given query:
+    #
+    #   /gokmen/development/klient/0.1.79/public-region/gokmen/db4acb42-91de-4176-6bdb-5be303a67e17
+    #
+    # It produces and removes followings:
+    #
+    #   KITE_/gokmen//////
+    #   KITE_///////db4acb42-91de-4176-6bdb-5be303a67e17
+    #   KITE_/gokmen/development/////
+    #   KITE_//////gokmen/db4acb42-91de-4176-6bdb-5be303a67e17
+    #   KITE_/gokmen/development/klient////
+    #   KITE_/////public-region/gokmen/db4acb42-91de-4176-6bdb-5be303a67e17
+    #   KITE_/gokmen/development/klient/0.1.79///
+    #   KITE_////0.1.79/public-region/gokmen/db4acb42-91de-4176-6bdb-5be303a67e17
+    #   KITE_/gokmen/development/klient/0.1.79/public-region//
+    #   KITE_///klient/0.1.79/public-region/gokmen/db4acb42-91de-4176-6bdb-5be303a67e17
+    #   KITE_/gokmen/development/klient/0.1.79/public-region/gokmen/
+    #   KITE_//development/klient/0.1.79/public-region/gokmen/db4acb42-91de-4176-6bdb-5be303a67e17
+    #   KITE_/gokmen/development/klient/0.1.79/public-region/gokmen/db4acb42-91de-4176-6bdb-5be303a67e17
+    #   KITE_/gokmen/development/klient/0.1.79/public-region/gokmen/db4acb42-91de-4176-6bdb-5be303a67e17
+
+    for part, i in queryA = (query.split '/')[1..]
+      ltr += "/#{part}"
+      delete storage[signature + ltr + ('/' for x in [i...6]).join '']
+      rtl = "/" + queryA[queryA.length - i - 1] + rtl
+      delete storage[signature + (('/' for x in [i...6]).join '') + rtl]
+
     return


pricing: update the plan prices

pricing: update the plan prices

--- client/Pricing/views/plans.coffee
+++ client/Pricing/views/plans.coffee
@@ -36,10 +36,10 @@
     ]
   ,
     title        : 'Hobbyist'
-    monthPrice   : 995
-    reducedMonth : 896
-    yearPrice    : 10789
-    discount     : 100
+    monthPrice   : 1250
+    reducedMonth : 995
+    yearPrice    : 11940
+    discount     : 255
     description  : 'Best for expanded learning or for running a small blog/website'
     cssClass     : 'hobbyist'
     planFeatures : [
@@ -51,10 +51,10 @@
     ]
   ,
     title        : 'Developer'
-    monthPrice   : 1995
-    reducedMonth : 1796
-    yearPrice    : 21589
-    discount     : 200
+    monthPrice   : 2450
+    reducedMonth : 1995
+    yearPrice    : 23940
+    discount     : 455
     description  : 'Great for developers who work with multiple environments'
     cssClass     : 'developer'
     planFeatures : [
@@ -66,10 +66,10 @@
     ]
   ,
     title        : 'Professional'
-    monthPrice   : 4995
-    reducedMonth : 4496
-    yearPrice    : 53989
-    discount     : 500
+    monthPrice   : 4950
+    reducedMonth : 3995
+    yearPrice    : 47940
+    discount     : 955
     description  : 'Great for managing and delivering client work'
     cssClass     : 'professional'
     planFeatures : [


Fix react warnings

Fix react warnings

--- client/app/components/menu.coffee
+++ client/app/components/menu.coffee
@@ -108,8 +108,8 @@

                 if @state.accounts.length
                     @state.accounts
-                    .sort @selectedFirstSort.bind @
-                    .map @getAccountRender.bind @
+                    .sort @selectedFirstSort
+                    .map @getAccountRender
                     .toJS()

             nav className: 'submenu',
@@ -295,7 +295,7 @@
                             a
                                 role: 'menuitem',
                                 tabIndex: 0,
-                                onClick: @_toggleFavorites.bind(@),
+                                onClick: @_toggleFavorites,
                                 key: 'toggle',
                                     i className: 'fa ' + icon
                                     span


Fix typo

Fix typo

--- client/components/TopPanel.coffee
+++ client/components/TopPanel.coffee
@@ -23,7 +23,7 @@
 import UserName, {color} from './UserName'

 import ConnectedComponent from '../lib/ConnectedComponent'
-import {getClassStartingFromJuly} from '..//lib/graduateYearToClass'
+import {getClassStartingFromJuly} from '../lib/graduateYearToClass'
 import isPaid, {unpaidBlocked} from '../lib/isPaid'
 import needDeactivatedWarning from '../lib/needDeactivatedWarning'


implement things to actual app

implement things to actual app

--- public/coffeescript/app.coffee
+++ public/coffeescript/app.coffee
@@ -19,17 +19,20 @@

   # Services
   require('./document/DocService')
+  require('./document/WordCountService')
   require('./user/UserService')

   # Controllers
   require('./base/BaseCtrl')
-  # require('./user/UserController')
+  require('./user/UserController')
   require('./document/DocumentsController')

   # Directives
   require('./base/directives/MenuToggle')
+  require('./base/directives/SettingsToggle')
+  require('./base/directives/Switch')
   require('./base/directives/Preview')
-  require('./base/directives/Wordcount')
+  # require('./base/directives/Wordcount')
   require('./document/directives/DocumentTitle')

   angular.bootstrap document, ['Dillinger']


Add parsing test

Add parsing test

--- spec/countries/DOM-spec.coffee
+++ spec/countries/DOM-spec.coffee
@@ -8,6 +8,16 @@
 		result = Phone.validate(number, '1')

 		expect(result).to.be.true
+
+	it 'should parse number to DOM', ->
+		number = '+1 809 53 55555'
+
+		result = Phone.getPhoneInternational(number)
+
+		expect(result.countryNameAbbr).to.equal('DOM')
+		expect(result.countryCode).to.equal('1')
+		expect(result.nationalDestinationCode).to.equal('809')
+		expect(result.number).to.equal('5355555')

 	it 'should validate 829 ndc', ->
 		number = '+1 829 53 55555'


Add missing debug pass through

Add missing debug pass through

--- app.coffee
+++ app.coffee
@@ -12,7 +12,7 @@
   .option('-d, --debug', "Output route matching debug info", false)
   .parse(process.argv)

-options = _.pick commander, 'port'
+options = _.pick commander, 'port', 'debug'

 try
   cfg = fs.readFileSync('.vee').toString('utf8')


Only send battle chat messages to certain players.

--- app.coffee
+++ app.coffee
@@ -62,7 +62,13 @@
       user.send 'error', 'ERROR: Battle does not exist'
       return

-    user.broadcast 'update battle chat', battleId, user.toJSON(), message
+    # TODO: Make nicer interface
+    for player in battle.battle.players
+      continue  if player.user == user
+      player.send('update battle chat', battleId, user.toJSON(), message)
+    for spectator in battle.battle.spectators
+      continue  if spectator == user
+      spectator.send('update battle chat', battleId, user.toJSON(), message)

   # TODO: Dequeue player
   'close': (user) ->


Import the REST package.

Import the REST package.

--- test/e2e/helpers/seed.coffee
+++ test/e2e/helpers/seed.coffee
@@ -3,14 +3,10 @@
 fs = require 'fs'
 exec = require('child_process').exec

-restHome = path.normalize(path.join(__dirname, '../../../../qiprofile-rest'))
+option = 'from qiprofile_rest.test.helpers import seed; seed.seed()'

-seed = path.join(restHome, 'qiprofile_rest/test/helpers/seed.py')
+cmd = "python -c '#{ option }'"

-fs.statSync seed, (error, stats) ->
-  if error
-      throw "Test preparation seed script not found: #{ seed }"
-
-exec seed, (error) ->
+exec cmd, (error) ->
   if error
     throw "Test preparation seed script unsuccessful: #{ error }"


Revert "Add componentWillReceiveProps method"

This reverts commit c4a5a7f38247a797a5d3248e68bc662719bea8cc.

--- public/src/queue.cjsx
+++ public/src/queue.cjsx
@@ -32,12 +32,9 @@

 ModifyQueueButton = React.createClass
   getInitialState: ->
+    console.log @props.isPrivate
     email: ''
     isPrivate: @props.isPrivate
-
-  componentWillReceiveProps: ->
-    @setState
-      isPrivate: @props.isPrivate

   submitAddOwner: (ev) ->
     ev.preventDefault()


Removed extra logging

Removed extra logging

--- reactive-table.coffee
+++ reactive-table.coffee
@@ -3,9 +3,7 @@
 Template.ReactiveTable.helpers
   data: ->
     @_table = new ReactiveTable @options
-    log = @_table.getData()
-    console.log log
-    log
+    @_table.getData()

 Template.ReactiveTable.rendered = ->
   console.log "table rendered"


Update hardcoded_triggers.coffee (#85)

Update hardcoded_triggers.coffee (#85)

fix broken 'phrasing' link

--- support/hardcoded_triggers.coffee
+++ support/hardcoded_triggers.coffee
@@ -36,7 +36,7 @@
   '!goal'         : "GOOOOOOOOOOOOOOOOOOOOOOAAAAAAAAAAAAAAAAALLLLLLLLLL!!!!!!!"
   '!unstoppable'  : "I am unstoppable!!! http://i.imgur.com/ALHS4Za.png"
   '!haha'         : "http://i.imgur.com/CVhoCwr.jpg"
-  '!phrasing'     : "Phrasing! http://batdoc.files.wordpress.com/2012/03/archer-1-phrasing.jpg"
+  '!phrasing'     : "Phrasing! https://getyarn.io/yarn-clip/715503bc-923d-46c1-a89b-a5c0c48d7c7a"

 # Bot Info
 nick = process.env.HUBOT_IRC_NICK?.toLowerCase() ? "hubot"


remove stopPropagation

remove stopPropagation

--- app/assets/javascripts/active_admin/lib/dropdown-menu.js.coffee
+++ app/assets/javascripts/active_admin/lib/dropdown-menu.js.coffee
@@ -65,8 +65,7 @@
     $('body').click =>
       @close() if @isOpen

-    @$menuButton.click (e)=>
-      e.stopPropagation()
+    @$menuButton.click =>
       unless @isDisabled()
         if @isOpen then @close() else @open()
       false


allow need_help tasks to be uploaded

allow need_help tasks to be uploaded

--- src/app/tasks/partials/directives/submit-task-form.coffee
+++ src/app/tasks/partials/directives/submit-task-form.coffee
@@ -115,6 +115,9 @@
       else
         $scope.payload = {}

+      if $scope.uploadType == 'need_help'
+        $scope.payload.trigger = 'need_help'
+
     $scope.inGroupForTask = () ->
       projectService.getGroupForTask($scope.project, $scope.task)?