use component's full name to avoid confusion

--- tutor/src/components/reference-book/reference-book.cjsx
+++ tutor/src/components/reference-book/reference-book.cjsx
@@ -9,7 +9,7 @@
 Menu = require './slide-out-menu'
 {ResizeListenerMixin} = require 'shared'
 PageShell = require './page-shell'
-PageNavigation = require './page-navigation'
+ReferenceViewPageNavigation = require './page-navigation'

 # menu width (300) + page width (1000) + 50 px padding
 # corresponds to @reference-book-page-width and @reference-book-menu-width in variables.less
@@ -91,12 +91,12 @@
             onMenuSelection={@onMenuClick}
           />

-          <PageNavigation
+          <ReferenceViewPageNavigation
             {...pageProps}
             pageNavRouterLinkTarget={@props.pageNavRouterLinkTarget}
           >
             <@props.contentComponent {...pageProps} />
-          </PageNavigation>
+          </ReferenceViewPageNavigation>

         </div>


fixed horizontal centering

fixed horizontal centering

--- web/scripts/cloud.coffee
+++ web/scripts/cloud.coffee
@@ -95,9 +95,8 @@
 	$(obj).append(cont)
 	text.innerHTML = "
"+par[0]+"
"
 	$(text).css({'font-size':(dim*.05*tscale) + 'px'})
-
-	textwidth=text.clientWidth
-	left=0.5*dim-0.5*textwidth
+	# Width is 75%, leaving 25% left to play with
+	left=0.125*dim
 	$(text).css({"left":left+"px"})
 	#Removed vertical centering - looks better without it
 	#"top":(dim*0.8-$(text).height())/2+"px",


Update progress on file removal, too

Update progress on file removal, too

Remove unused variable

--- web/volume/assist.coffee
+++ web/volume/assist.coffee
@@ -40,6 +40,11 @@
       slot = volume.top
       $scope.assets = slot.assets
       $scope.uploads = []
+      $scope.progress = 0
+
+      remove = (file) ->
+        $scope.uploads.remove(file)
+        $scope.progress = file.flowObj.progress()

       $scope.fileAdded = (file) ->
         $scope.uploads.push(file)
@@ -52,7 +57,7 @@
               body: constants.message('asset.upload.rejected', {sce:$sce.HTML}, file.relativePath)
               report: res
               owner: this
-            $scope.uploads.remove(file)
+            remove(file)
             return
         return

@@ -65,7 +70,7 @@
             classification: null
           ).then (asset) ->
               file.cancel()
-              $scope.uploads.remove(file)
+              remove(file)
               return
             , (res) ->
               messages.addError
@@ -76,7 +81,7 @@
               file.cancel()
               file.progressValue = null
               return
-        $scope.uploads.remove(file)
+        remove(file)
         return

       $scope.fileProgress = (file) ->
@@ -90,10 +95,8 @@
           body: constants.message('asset.upload.error', file.relativePath, message || 'unknown error')
           owner: form
         file.cancel()
-        $scope.uploads.remove(file)
+        remove(file)
         return
-
-      $scope.filesCount = Object.keys($scope.assets).length

       form.questions =
         [


fuck everything pls why god dammit

fuck everything pls why god dammit

--- src/lib/client/opened_apps.coffee
+++ src/lib/client/opened_apps.coffee
@@ -73,7 +73,7 @@
   get_firebase_value: () ->
     return 1

-to_firebase_dict: (apps) ->
+to_firebase_dict = (apps) ->
   dict = {}
   for app in apps
     dict[app.get_firebase_key()] = app.get_firebase_value()


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpie_agent.coffee
+++ widgets/fullpie_agent/fullpie_agent.coffee
@@ -16,6 +16,8 @@
     $(@node).children(".more-info").text($(@node).attr("data-moreinfo"))
     $(@node).children(".updated-at").text(@get('updatedAtMessage'))

+    mFontSize = 28
+
     width = 400 #width
     height = 400 #height
     radius = 200 #radius
@@ -50,20 +52,6 @@
     for val in data
       sum += val.value

-# Core
-#    arcs.append("svg:text").attr("transform", (d, i) ->
-#      procent_val = Math.round(data[i].value/sum * 100)
-#      d.innerRadius = (radius * (100-procent_val)/100) - 45  #45=max text size/2
-#      d.outerRadius = radius
-#      "translate(" + arc.centroid(d) + ")")
-#      .attr('fill', "#fff")
-#      .attr("text-anchor", "middle").text((d, i) -> data[i].label).attr('font-size', '28px')
-#      .append('svg:tspan')
-#      .attr('x', 0)
-#      .attr('dy', 25)
-#      .attr('font-size', '90%')
-#      .text((d,i) -> data[i].value + ' (' + Math.round(data[i].value/sum * 100) + '%)')
-
     if !sum
       arcs.append("svg:text")
         .attr('fill', "#fff")
@@ -78,7 +66,7 @@
         .attr("text-anchor", "middle").text((d, i) ->
           if data[i].value != 0
             data[i].label
-        ).attr('font-size', '28px')
+        ).attr('font-size', mFontSize + 'px')
         .append('svg:tspan')
         .attr('x', 0)
         .attr('dy', '.9em')
@@ -87,4 +75,3 @@
           if data[i].value != 0
             data[i].value + ' (' + Math.round(data[i].value/sum * 100) + '%)'
 		)
-


vm: remove the user-related hooks

vm: remove the user-related hooks

- seems like there is no reason for those.
- also try to work around a bug in bongo defaults.  let me look into that later.

--- workers/social/lib/social/models/vm.coffee
+++ workers/social/lib/social/models/vm.coffee
@@ -12,10 +12,10 @@
     schema            :
       ip              :
         type          : String
-        default       : null
+        default       : -> null
       ldapPassword    :
         type          : String
-        default       : null
+        default       : -> null
       name            : String
       users           : Array
       groups          : Array
@@ -44,12 +44,6 @@
       vm.save (err)-> target.addVm vm, handleError

     wrapGroup =(group)-> [ { id: group.getId() } ]
-
-    JUser.on 'UserCreated', (user)->
-      console.warn 'User created hook needs to be implemented.'
-
-    JUser.on 'UserDestroyed', (user)->
-      console.warn 'User destroyed hook needs to be implemented.'

     JGroup.on 'GroupCreated', ({group, creator})->
       creator.fetchUser (err, user)->


Lasso

Lasso

- Wait for "selected" to finish (also Deferred), before resetting
  lassoed objects. This avoids flicker in "ng" design.

--- src/base/DragDropSelect/Lasso.coffee
+++ src/base/DragDropSelect/Lasso.coffee
@@ -112,10 +112,16 @@
 		lassoed

 	end_drag: (ev) ->
-		@resetLassoedElements()
 		if ev.getType() == "mouseup"
 			globalDrag.elements = @get_lassoed_elements()
-			@_selected(ev, globalDrag)
+			ret = @_selected(ev, globalDrag)
+			if isPromise(ret)
+				ret.always =>
+					@resetLassoedElements()
+			else
+				@resetLassoedElements()
+		else
+			@resetLassoedElements()
 		globalDrag.lasso.remove()


Amend grading report tooltip further

Amend grading report tooltip further

--- src/js/app/helpers/handlebars.coffee
+++ src/js/app/helpers/handlebars.coffee
@@ -55,27 +55,22 @@
   count = safe + unsafe
   rate = 100 - ((unsafe / (safe + unsafe)) * 100)
   rateFixed = switch
-    when isNaN(rate) then "not applicable as we detected URL redirection"
+    when isNaN(rate) then "not applicable"
     else "#{rate.toFixed(1)}%"
-  grade = switch
-      when rate >= 93 then "A"
-      when rate >= 83 then "B"
-      when rate >= 73 then "C"
-      when rate >= 63 then "D"
-      when isNaN(rate) is true then "R"
-      else "F"
   html = """

Grading

+
Your grade is based on the (1) number of unsafe assets and the (2) total number of assets

+
We found #{unsafe} unsafe assets out of the total of #{count} assets

+
The rate of these two numbers is #{rateFixed}, which provides you the final grade as below:

A : 93% — 100%

B : 83% — 93%

C : 73% — 83%

D : 63% — 73%

F : 0% — 63%

+
R : URL redirection was detected

+
NA : Assets were not detected

-
Your grade is based on the (1) number of unsafe assets and the (2) total number of assets

-
We found #{unsafe} unsafe assets out of the total of #{count} assets

-
The rate of these two numbers is #{rateFixed}, which provides your grade: '#{grade}'

   """
   return html


Fix issue with non-movement

Fix issue with non-movement

--- src/lib/script_modules/entity.coffee
+++ src/lib/script_modules/entity.coffee
@@ -239,7 +239,6 @@
           return @_moveAir(airSettings)
             .then =>
               @enemy.moveState = 'water'
-              #if @enemy.alive
               @_setupWaterSpot()
               @_moveWater(settings)
         else
@@ -252,7 +251,6 @@
           return @_moveWater(waterSettings)
             .then =>
               @enemy.moveState = 'air'
-              #if @enemy.alive
               @_removeWaterSpot()
               @_moveAir(settings)
         else
@@ -400,6 +398,7 @@
     deltaX = if settings.x? then Math.abs(settings.x - @entity.x) else 0
     deltaY = if settings.y? then Math.abs(settings.y - @entity.y) else 0
     delta = Math.sqrt((deltaX ** 2) + (deltaY ** 2))
+    return Promise.resolve() if delta == 0

     return new Promise((resolve) =>
       easing = settings.easing ? 'linear'


Removing purposeful error

Removing purposeful error

--- tests/www/tests/functional/routing.coffee
+++ tests/www/tests/functional/routing.coffee
@@ -27,7 +27,7 @@
       # ------------------------------------------------------------------------
       describe 'menu must to exist', ->
         it 'wait until menu is visible', (done)->
-          browser.waitForElementByClassName 'menu-x', timeout, (err)->
+          browser.waitForElementByClassName 'menu', timeout, (err)->
             should.not.exist err
             browser.elementByClassName 'menu', (err, el)->
               should.not.exist err


Fix a scrollable issue.

Fix a scrollable issue.

--- src/coffee/kopi/ui/scrollable.coffee
+++ src/coffee/kopi/ui/scrollable.coffee
@@ -122,7 +122,6 @@
           self._container.unbind(events.WEBKIT_TRANSITION_END_EVENT)
           self._steps = []
           self._position(matrix.x, matrix.y)
-      super

     ontouchmove: (e, event) ->
       event.preventDefault()
@@ -166,7 +165,6 @@
         self._startTime = timestamp
         self._startX = self._x
         self._startY = self._y
-      super

     ontouchend: (e, event) ->
       cls = this.constructor
@@ -208,7 +206,6 @@
         if (self._y > self._minScrollY and newY > self._minScrollY) or (self._y < self._maxScrollY and newY < self._maxScrollY)
           momentum.distY = 0

-      super

       if momentum.distX or momentum.distY
         newDuration = math.max(momentum.duration, 10)
@@ -228,7 +225,6 @@

     ontouchcancel: ->
       this.ontouchend(arguments...)
-      super

     ontransitionend: (e, event) ->
       cls = this.constructor


Use npm icon for package-lock.json (#593)

Introduced by npm 5: https://github.com/npm/npm/pull/16244

--- config.cson
+++ config.cson
@@ -2768,7 +2768,7 @@
 	NPM:
 		icon: "npm"
 		priority: 2
-		match: /^(package\.json|\.npmignore|\.?npmrc|npm-debug\.log|npm-shrinkwrap\.json)$/i
+		match: /^(package\.json|\.npmignore|\.?npmrc|npm-debug\.log|npm-shrinkwrap\.json|package-lock\.json)$/i
 		colour: "medium-red"

 	NSIS:


Awesome refactor.

Awesome refactor.

--- app/client/js/app.coffee
+++ app/client/js/app.coffee
@@ -4,23 +4,21 @@

   ###*
     @param {Element} element
-    @param {app.Dispatcher} dispatcher
     @param {app.Routes} routes
     @param {app.Storage} storage
     @param {app.react.App} reactApp
     @param {este.Router} router
     @constructor
   ###
-  constructor: (element, dispatcher, routes, storage, reactApp, router) ->
+  constructor: (element, routes, storage, reactApp, router) ->

     routes.addToEste router, (route, params) ->
-      dispatcher.dispatch 'route-load', route: route, params: params
-
-    dispatcher.register (action, payload) ->
-      if action == 'route-load'
-        storage.load payload.route, payload.params
-          .then -> routes.setActive payload.route, payload.params
-          .thenCatch (reason) -> routes.trySetErrorRoute reason
-          .then -> React.renderComponent reactApp.component(), element
+      storage.load route, params
+        # Set active route if everything is fine.
+        .then -> routes.setActive route, params
+        # Try set error route for known errors. Only 404 for now.
+        .thenCatch (reason) -> routes.trySetErrorRoute reason
+        # Everything is ok, rerender view.
+        .then -> React.renderComponent reactApp.component(), element

     router.start()


Change text of jobs

Change text of jobs

--- config/jobs.cson
+++ config/jobs.cson
@@ -27,7 +27,9 @@
     "おはようさんです！今日は何しますか？ @everyone"]

 review:
-  time: "0 18 * * 1-5"
+  time: "27 18 * * 1-5"
   message: [
-    "お疲れ様です。今日の目標は達成できましたか？ @everyone",
-    "もう夕方ですね、今日はどんな事をしましたか？ @everyone"]
+    "お疲れ様です。今日の目標は達成できましたか？ @everyone\n
+    「目標達成」と話しかけてくださいね。",
+    "もう夕方ですね、今日はどんな事をしましたか？ @everyone\n
+    目標達成って叫んでも良いですよ！"]


Allow pressing down on input to search with previous query. Closes #13

--- jquery.immybox.coffee
+++ jquery.immybox.coffee
@@ -50,6 +50,7 @@
       @hideResults()

       @_val = @element.val() # to keep track of what WAS in the text box
+      @oldQuery = @_val

       @queryResultArea.on 'mousedown', 'li.choice', ->
         value = $(@).data 'value'
@@ -80,22 +81,11 @@
       query = @element.val()
       if @_val isnt query # user has changed input value, do a search and display the search results
         @_val = query
+        @oldQuery = query
         if query is ''
           @hideResults()
         else
-          filteredChoices = (@choices.filter @options.filterFn query)
-          truncatedChoices = filteredChoices[0...@options.maxResults]
-          difference = filteredChoices.length - truncatedChoices.length
-          results = truncatedChoices.map (choice) -> "
#{esc choice.text}
"
-          info = if difference > 0
-            "
showing #{addCommas truncatedChoices.length} of #{addCommas filteredChoices.length}
"
-          else if results.length is 0
-            "
no matches
"
-          else
-            ''
-          @queryResultArea.html "
#{results.join '\n'}
#{info}"
-          @queryResultArea.find('li.choice:first').addClass 'active'
-          @showResults()
+          @insertFilteredChoiceElements query

     # @element.on 'keydown'
     # select the highlighted choice
@@ -116,6 +106,13 @@
           when 40 # down
             e.preventDefault() # prevent cursor from moving
             @highlightNextChoice()
+      else
+        switch e.which
+          when 40
+            if @selectedChoice?
+              @insertFilteredChoiceElements @oldQuery
+            else
+              @insertFilteredChoiceElements ''

     # @element.on 'blur'
     # revert or set to null after losing focus
@@ -137,6 +134,24 @@
     ###################
     # private methods #
     ###################
+
+    insertFilteredChoiceElements: (query) ->
+      if query is ''
+        filteredChoices = @choices
+      else
+        filteredChoices = (@choices.filter @options.filterFn @oldQuery)
+      truncatedChoices = filteredChoices[0...@options.maxResults]
+      difference = filteredChoices.length - truncatedChoices.length
+      results = truncatedChoices.map (choice) -> "
#{esc choice.text}
"
+      info = if difference > 0
+        "
showing #{addCommas truncatedChoices.length} of #{addCommas filteredChoices.length}
"
+      else if results.length is 0
+        "
no matches
"
+      else
+        ''
+      @queryResultArea.html "
#{results.join '\n'}
#{info}"
+      @queryResultArea.find('li.choice:first').addClass 'active'
+      @showResults()

     positionResultsArea: ->


.

Former-commit-id: 0daec3873d7950978fc1844b9780a8211b71013e [formerly f411a329205fbf492d3b510910849e5ca8447c9c] [formerly c353436621d2c720e09baa3a93a672a12708530a [formerly f7ab6f12827cafbb80b8cfa9276bc21138e477fc]] [formerly d15b4387169688f797fd17f7e028b14e42b7d23b [formerly cd5c2183cb4fb24374e44af87f22d46f9e5c8a7d] [formerly a916ea3e8c89f6203ac9bf4b905cc00264a0b80b [formerly a916ea3e8c89f6203ac9bf4b905cc00264a0b80b [formerly 6289fd475df0bb137b35feceaa3632948b5838e2]]]]
Former-commit-id: 81cfb7ee9e1267893ae418ed1a916f23b8dd13ae [formerly eba2d0f4688462cef527629bfe0d805b3bb18896] [formerly ff53ca0edaaad2ff9d2a48eaaddbf17fe18ffe35 [formerly b7fec8e08e78944e02915900badb020a64ece66b]]
Former-commit-id: 04cd9511198aea81801c2fe39fc926c48a44295e [formerly fc27e9a5fdb72ed4020dc96dd85fa40d55901176]
Former-commit-id: 00338a825768c2b5071e3b23ac8c880fb1eaa7da
Former-commit-id: a939a4edccdf2c81e04ae175bab61f4379395b4d

--- @client/element_viewport_positioning.coffee
+++ @client/element_viewport_positioning.coffee
@@ -25,11 +25,6 @@
   bottom_inside = el_bottom < doc_bottom && (el_bottom - doc_top) > options.fill_threshold * el_height
   top_inside = el_top > doc_top && (doc_bottom - el_top) > options.fill_threshold * el_height
   in_viewport = is_onscreen || top_inside || bottom_inside
-
-  # console.log "amount: #{options.fill_threshold}"
-  # console.log "el_top: #{el_top}, el_bottom: #{el_bottom}"
-  # console.log "doc_top: #{doc_top}, doc_bottom: #{doc_bottom}"
-  # console.log "onscreen: #{is_onscreen}, top_inside: #{top_inside}, bottom_inside: #{bottom_inside}"

   if !in_viewport
     switch options.position


:lipstick:

:lipstick:

--- src/packages/spell-check/lib/spell-check-task.coffee
+++ src/packages/spell-check/lib/spell-check-task.coffee
@@ -2,7 +2,6 @@

 module.exports =
 class SpellCheckTask extends Task
-
   constructor: (@text, @callback) ->
     super('spell-check/lib/spell-check-handler')


add error checking to the url feed

add error checking to the url feed

--- modules/data-table/main/index.coffee
+++ modules/data-table/main/index.coffee
@@ -784,20 +784,22 @@
     else
       (cb) -> fetcher(cb)

+  jsonCallback = (cb) ->
+    (err, value) ->
+      console.error(err) if err
+      cb(value)
+
   {
     url: url # for debugging
     headers: maybeCached (cb) ->
-      hx.json url, { type: 'headers', extra: options.extra }, (err, res) ->
-        cb(res)
+      hx.json url, { type: 'headers', extra: options.extra }, jsonCallback(cb)
     totalCount: maybeCached (cb) ->
       hx.json url, { type: 'totalCount', extra: options.extra }, (err, res) ->
-        cb(res.count)
+        jsonCallback(cb)(err, res.count)
     rows: (range, cb) ->
-      hx.json url, { type: 'rows', range: range, extra: options.extra }, (err, res) ->
-        cb(res)
+      hx.json url, { type: 'rows', range: range, extra: options.extra }, jsonCallback(cb)
     rowsForIds: (ids, lookupRow, cb) ->
-      hx.json url, { type: 'rowsForIds', ids: ids, extra: options.extra }, (err, res) ->
-        cb(res)
+      hx.json url, { type: 'rowsForIds', ids: ids, extra: options.extra }, jsonCallback(cb)
   }


Only subscribe to running builds

Only subscribe to running builds

--- js/inner.coffee
+++ js/inner.coffee
@@ -329,12 +329,19 @@
         when "user-not-paid"
           "#{@author()}'s trial is over. Add them to your account"

-    @build_channel = VM.pusher.subscribe(@pusherChannel())
-    @build_channel.bind('pusher:subscription_error', (status) -> notifyError status)
-
-    @build_channel.bind('newAction', (json) => @newAction json)
-    @build_channel.bind('updateAction', (json) => @updateAction json)
-    @build_channel.bind('appendAction', (json) => @appendAction json)
+    @maybeSubscribe()
+
+  isRunning: () =>
+    @start_time and not @end_time()
+
+  maybeSubscribe: () =>
+    if @isRunning()
+      @build_channel = VM.pusher.subscribe(@pusherChannel())
+      @build_channel.bind('pusher:subscription_error', (status) -> notifyError status)
+
+      @build_channel.bind('newAction', (json) => @newAction json)
+      @build_channel.bind('updateAction', (json) => @updateAction json)
+      @build_channel.bind('appendAction', (json) => @appendAction json)

   newAction: (json) =>
     if not @steps()[json.step]?


Hello world from Pusher

Hello world from Pusher

--- js/inner.coffee
+++ js/inner.coffee
@@ -524,6 +524,24 @@
     @project_map = {}
     observableCount += 8

+    @pusher = new Pusher("356b7c379e56e14c261b")
+    @user_channel = @pusher.subscribe(@current_user().login)
+    @user_channel.bind("alert", (data) -> alert data.message )
+
+    @setupPusherChannels()
+
+  testCall: (arg) =>
+    alert(arg)
+
+  setupPusherChannels: () =>
+    @user_channel.bind "call", (data) =>
+      # alert("fn is " + this[data.fn])
+      # alert("args is " + data.args)
+      window.fn = data.fn
+      window.args = data.args
+      this[data.fn].apply(this, data.args)
+
+
   clearErrorMessage: () =>
     @error_message null


Better error message on donation failure

--- static/src/scripts/app/views/DonationFormView.coffee
+++ static/src/scripts/app/views/DonationFormView.coffee
@@ -92,7 +92,7 @@
       @l.setProgress 0.4

       # bundle up the stripe token
-      number = parseInt number.replace /\s/g, ''
+      number = parseInt number.replace /\s/g, '' # remove any space in card number
       stripeData =
         number: number
         cvc: cvc
@@ -135,7 +135,7 @@
           @donationThankYou()
         .fail (err) =>
           @submitted = false
-          @donateFormResponse("alert", "Donation failed. Try again.")
+          @donateFormResponse("alert", "Donation failed. Check your card details then try again")

     donationThankYou: =>
       $("#donate-content").animo


Revert "Input: Added a new attribute to change the input type to 'password' ('text' as default like before). See #52311"

This reverts commit eb489ff2ee11c0fe167987cca4d1d07b9a3dd062.

--- src/elements/Input/Input.coffee
+++ src/elements/Input/Input.coffee
@@ -143,9 +143,6 @@
 				check: Boolean
 			appearance:
 				check: ["code"]
-			input_type:
-				check: ["text", "password"]
-				default: "text"

 	readOpts: ->

@@ -226,7 +223,7 @@
 	# - up/down cursor number decrement/increment
 	# - input masking

-	__createElement: () ->
+	__createElement: (input_type="text") ->
 		if @_textarea ==  true
 			@__input = CUI.dom.$element "textarea", "cui-textarea",
 				placeholder: @getPlaceholder()
@@ -236,7 +233,7 @@
 				spellcheck: @__spellcheck
 		else
 			@__input = CUI.dom.$element "input", "cui-input",
-				type: @_input_type
+				type: input_type
 				size: 1
 				placeholder: @getPlaceholder()
 				tabindex: "0"


Fix bug for invisible frames

Fix bug for invisible frames

--- framer/Layer.coffee
+++ framer/Layer.coffee
@@ -1206,18 +1206,6 @@
 	##############################################################
 	## HINT

-	_visibleFrame: ->
-
-		# Figure out the frame we want to show the hint in, if any of the
-		# parent layers clip, we need to intersect the rectangle with it.
-		frame = @canvasFrame
-
-		for parent in @ancestors(context=true)
-			if parent.clip
-				 frame = Utils.frameIntersection(frame, parent.canvasFrame)
-
-		return frame
-
 	_showHint: (targetLayer) ->

 		# If this layer isnt visible we can just exit
@@ -1235,7 +1223,18 @@
 			layer._showHint(targetLayer) for layer in @children
 			return null

-		frame = @_visibleFrame()
+		# Figure out the frame we want to show the hint in, if any of the
+		# parent layers clip, we need to intersect the rectangle with it.
+		frame = @canvasFrame
+
+		return unless frame
+
+		for parent in @ancestors(context=true)
+			if parent.clip
+				parentFrame = parent.canvasFrame
+				if parentFrame
+					frame = Utils.frameIntersection(frame, parent.canvasFrame)
+
 		return unless frame

 		# Show the actual hint


Log source map only if it exists.

Log source map only if it exists.

--- src/fs_utils/source_file.coffee
+++ src/fs_utils/source_file.coffee
@@ -62,7 +62,8 @@
     cache.dependencies = dependencies
     cache.source = source
     cache.compilationTime = Date.now()
-    debug JSON.stringify( sourceMap)
+    if sourceMap
+      debug "Generated source map for '#{realPath}': " + JSON.stringify sourceMap
     cache.node = if sourceMap?
       map = new SourceMapConsumer sourceMap
       SourceNode.fromStringWithSourceMap compiled, map


Allow discussions to be moved

Allow discussions to be moved

--- app/talk/discussion.cjsx
+++ app/talk/discussion.cjsx
@@ -17,6 +17,7 @@
 talkConfig = require './config'
 SignInPrompt = require '../partials/sign-in-prompt'
 alert = require '../lib/alert'
+merge = require 'lodash.merge'

 PAGE_SIZE = talkConfig.discussionPageSize

@@ -170,12 +171,18 @@
   onEditSubmit: (e) ->
     e.preventDefault()
     form = document.querySelector('.talk-edit-discussion-form')
+    boardSelect = form.querySelector('select')
+
     title = form.querySelector('[name="title"]').value
     sticky = form.querySelector('[name="sticky"]').checked
     locked = form.querySelector('[name="locked"]').checked
-    @discussionsRequest().update({title, sticky, locked}).save()
+    board_id = boardSelect.options[boardSelect.selectedIndex].value
+
+    @discussionsRequest().update({title, sticky, locked, board_id}).save()
       .then (discussion) =>
-        @setState {discussion: discussion[0]}
+        {owner, name} = @props.params
+        discussionRoute = if (owner and name) then 'project-talk-discussion' else 'talk-discussion'
+        @transitionTo discussionRoute, merge(@props.params, board: board_id), @props.query

   lockedMessage: ->

@@ -202,6 +209,18 @@
                 Locked:
                   <input name="locked" type="checkbox" defaultChecked={discussion?.locked}/>

+
+                <PromiseRenderer promise={talkClient.type('boards').get({section: discussion.section, page_size: 100})}>{(boards) =>
+

+
Board:

+                    <select onChange={@onChangeSelect}>
+                      {boards.map (board, i) =>
+                        <option key={board.id} value={board.id} selected={board.id is discussion.board_id}>{board.title}</option>
+                        }
+                    </select>
+

+                }</PromiseRenderer>
+
                 <button type="submit">Update</button>
               </form>}


Don't actually request facebook in the tests

--- core/app/backbone/modules/facebook_renewal.coffee
+++ core/app/backbone/modules/facebook_renewal.coffee
@@ -1,9 +1,14 @@
 FactlinkApp.module "FacebookRenewal", (FacebookRenewal, FactlinkApp, Backbone, Marionette, $, _) ->
+
+  if Factlink.Global.environment == 'test'
+    renewal_url = "about:blank"
+  else
+    renewal_url = "/auth/facebook"

   class IframeView extends Backbone.Marionette.ItemView
     template:
       text: """
-
+
       """

   FacebookRenewal.addInitializer ->


use bindTo whenever possible

use bindTo whenever possible

--- core/app/backbone/views/fact_relation_view.coffee
+++ core/app/backbone/views/fact_relation_view.coffee
@@ -29,7 +29,7 @@
     "click .supporting": "believe"

   initialize: ->
-    @model.on "change", @render, @
+    @bindTo @model, "change", @render, @

   hideTooltips: ->
     @$(".weakening").tooltip "hide"


Add ping command.

Add ping command.

--- core/htdocs_source/src/pods/openxpki/route.coffee
+++ core/htdocs_source/src/pods/openxpki/route.coffee
@@ -18,6 +18,7 @@

     source: Em.computed -> Em.Object.create
         page: null
+        ping: null
         refresh: null
         structure: null
         status: null
@@ -32,7 +33,7 @@
         model_id = req.params.openxpki.model_id

         if not source.get("navEntries.length") or model_id in @needReboot
-            @sendAjax data:
+            @sendAjax data:
                 page: "bootstrap!structure"
                 baseurl: window.location.pathname

@@ -57,6 +58,14 @@
             data: data
         .then (doc) ->
             source
+
+    doPing: (cfg) ->
+        @set "source.ping", Em.run.later(@, ->
+            Em.$.ajax
+                url: cfg.href
+            doPing cfg
+        , cfg.timeout)
+

     sendAjax: (req) ->
         req.dataType = "json"
@@ -85,6 +94,11 @@

                 source.set "status", doc.status
                 source.set "modal", null
+
+                if doc.ping
+                    if source.get "ping"
+                        Em.run.cancel source.get "ping"
+                    @doPing doc.ping

                 if doc.refresh
                     source.set "refresh", Em.run.later(@, ->


ISSUE-57

ISSUE-57

- code rafactoring

--- www_src/locales.coffee
+++ www_src/locales.coffee
@@ -50,7 +50,7 @@
   #	@param {String} applicationTitle
   #	@return {Locale} Locale object or null
   # @see http://tools.ietf.org/html/bcp47
-  @getLocale: (language, applicationTitle = null) ->
+  @getLocale: (language, applicationTitle = '') ->
     localeObject = locales[language] or locales[language.split(/-/)[0]] or locales[LOCALE_DEFAULT]
     localeObject.title = localeObject.title.replace /%@/g, applicationTitle
     localeObject.message = localeObject.message.replace /%@/g, applicationTitle
@@ -60,7 +60,7 @@
   # Get list of locales names
   # @return [Array<String>] List of locales names
   @getLocalesNames: ->
-    k for k of locales
+    locale for locale of locales

 Locales.addLocale new Locale


Expose currentPage as a public API

Expose currentPage as a public API

--- lib/assets/javascripts/relax/start.coffee
+++ lib/assets/javascripts/relax/start.coffee
@@ -57,4 +57,4 @@
 Relax.ProgressBar = ProgressBarAPI
 Relax.supported = Relax.Utils.browserSupportsRelax()
 Relax.EVENTS = Relax.Utils.clone(EVENTS)
-
+Relax.currentPage = controller.currentPage


Soon.

Soon.

--- app/client/js/storage.coffee
+++ app/client/js/storage.coffee
@@ -19,7 +19,6 @@

   init: ->
     @localStorage.sync [@userStore]
-
     @dispatcher.register (action, payload) =>
       switch action
         when app.Actions.LOAD_ROUTE
@@ -61,3 +60,12 @@
             @songsStore.fromJson recentlyUpdatedSongs: songs
       else
         @notFound()
+
+#     when actions.PUBLISH_SONG
+#       @xhr
+#         .put @routes.api.songs.id.url(id: payload.song.id), payload.json
+#         .then => @userStore.setSongPublisher payload.song
+#     when actions.UNPUBLISH_SONG
+#       @xhr
+#         .delete @routes.api.songs.id.url(id: payload.song.id)
+#         .then => @userStore.removeSongPublisher payload.song


Campfire done

Campfire done

--- plugins/campfire/index.coffee
+++ plugins/campfire/index.coffee
@@ -3,9 +3,10 @@
 class Campfire extends NotificationPlugin
   @receiveEvent: (config, event) ->
     # Build the message
-    message =  "#{event.trigger.name} in #{event.project.name}" + ( if event.error then " in #{event.error.context} (#{event.error.url})" else "")
-    message += "\n#{event.error.exceptionClass}" + (if event.error.message then ": #{event.error.message}" else "") if event.error
-    message += "\n#{@firstStacktraceLine(event.error.stacktrace)}" if event.error && event.error.stacktrace
+    if event.error
+      message = "#{event.error.exceptionClass}" + (if event.error.message then ": #{event.error.message}" else "") + " (#{event.error.url})"
+    else
+      message =  "#{event.trigger.name} in #{event.project.name} (#{event.project.url})" + ( if event.error then " in #{event.error.context} (#{event.error.url})" else "")

     # Build the request
     payload =


'Updating for version 0.12.4dev2'

'Updating for version 0.12.4dev2'

--- bokehjs/src/coffee/version.coffee
+++ bokehjs/src/coffee/version.coffee
@@ -1 +1 @@
-export version = '0.12.4dev1'
+export version = '0.12.4dev2'


Oops! The modal has to be hidden after the task has been done.

--- app/controllers/modal.coffee
+++ app/controllers/modal.coffee
@@ -63,4 +63,6 @@
           currentNote.deleteNote()
           currentNote.destroy()

+          @hide()
+


So Nitro iOS doesn't shit it's pants - obj-c's fault

--- app/controllers/tasks.coffee
+++ app/controllers/tasks.coffee
@@ -186,6 +186,8 @@
         name: val
         list: @list?.id
         completed: false
+        notes: ""
+        date: ""
         priority: (->
           if val.indexOf("#high") >= 0 then return 3
           if val.indexOf("#medium") >= 0 then return 2


Update de-DE.coffee

Update de-DE.coffee

Just did the changes. I don't know if it is good to translate the job benefits, because some of them don't work in Germany and I think if someone wants to work for you they have to at least speak enough English to understand this benefits.

--- app/locale/de-DE.coffee
+++ app/locale/de-DE.coffee
@@ -616,11 +616,8 @@
     careers_link: "Karrieren"
     press_link: "Presse"
     mission_title: "Unsere Mission: Programmieren jedem Schüler auf der Erde zugänglich zu machen."
-    mission_description_1: "Programmieren ist Magie. Es ist die Fähigkeit Dinge aus purer Vorstellungskraft zu erschaffen.
-    Wir starteten CodeCombat um Lernen dieses Gefühl der Zauberkraft in ihren Fingerspitzen zu geben während sie getippten Code benutzen." # {change}
-    mission_description_2: "Wie sich herausstellt, können sie dadurch auch schneller lernen. Sehr viel schneller.
-    Es ist mehr wie eine Unterhaltung als das Lesen einer Bedienungsanleitung. Wir wollen diese Unterhaltung in jede Schule zu
-    jedem Schüler bringen, weil jeder die Chance haben sollte die Magie des Programmierens zu erlernen." # {change}
+    mission_description_1: "Programmieren ist Magie. Es ist die Fähigkeit Dinge aus purer Vorstellungskraft zu erschaffen. Wir starteten CodeCombat um Lernen dieses Gefühl der Zauberkraft in ihren Fingerspitzen zu geben während sie getippten Code benutzen." # {change}
+    mission_description_2: "Wie sich herausstellt, können sie dadurch auch schneller lernen. Sehr viel schneller.  Es ist mehr wie eine Unterhaltung als das Lesen einer Bedienungsanleitung. Wir wollen diese Unterhaltung in jede Schule zu jedem Schüler bringen, weil jeder die Chance haben sollte die Magie des Programmierens zu erlernen." # {change}
     team_title: "Triff das CodeCombat Team"
     team_values: "Wir legen Wert auf einen offenen und respektvollen Dialog, wo die besten Ideen gewinnen. Unsere Entscheidungen basieren auf Benutzerforschung und unser Fortschritt richtet sich danach ihnen handfeste Resultate zu liefern. Jeder packt mit an von unserem Geschäftsführer bis hin zu unseren  Github-Unterstützern, weil wir Wert auf Zuwachs und Lernen in unserem Team legen."
     nick_title: "Mitbegründer, Geschäftsführer" # {changed}
@@ -655,8 +652,8 @@
     number_contributors: "Mehr als 450 Unterstützer haben ihre Zeit und Hilfe für dieses Projekt zur Verfügung gestellt."
     story_title: "Unsere bisherige Geschichte"
     story_subtitle: "Seit 2013 ist CodeCombat aus einigen Skizzen zu einem lebenden, florierenden Spiel gewachsen."
-   story_statistic_1a: "Mehr als 5,000,000"
-  story_statistic_1b: "Spieler"
+    story_statistic_1a: "Mehr als 5,000,000"
+    story_statistic_1b: "Spieler"
     story_statistic_1c: "haben ihre Programmierreise durch CodeCombat begonnen."
     story_statistic_2a: "Wir wurden in über 50 Sprachen übersetzt — unsere Spieler kommen aus mehr als"
     story_statistic_2b: "200 Staaten"
@@ -667,8 +664,8 @@
     story_sketch_caption: "Nick's allererste Skizze zeigt ein Programmierspiel in Aktion."
     story_long_way_2: "haben wir noch viel zu tun bevor unser Auftrag abgeschlossen ist, also..."
     jobs_title: "Komm arbeite mit uns und hilf mit CodeCombat Geschichte zu schreiben!"
-#    jobs_subtitle: "Don't see a good fit but interested in keeping in touch? See our \"Create Your Own\" listing."
-#    jobs_benefits: "Employee Benefits"
+    jobs_subtitle: "Nichts gutes gefunden aber du bist trotzdem interessiert mit uns zu arbeiten? Schau dir unseren \"Kreiere deinen Eigenen\" Eintrag an."
+    jobs_benefits: "Angestellten Vorteile"
 #    jobs_benefit_1: "Competitive salary and options"
 #    jobs_benefit_2: "15 day minimum vacation policy, excluding company holidays"
 #    jobs_benefit_3: "Work from home flexibility"
@@ -703,7 +700,7 @@
     more_info_2: "Lehrerforum"
     more_info_3: "ist eine gute Anlaufstelle, um mit anderen Lehrer in Kontakt zu treten, die CodeCombat benutzen."

-#  teachers_quote:
+  teachers_quote:
 #    name: "Quote Form"
 #    title: "Request a Quote"
 #    subtitle: "Get CodeCombat in your classroom, club, school or district!"


fixing text overflow in pt-br content e translating content.

--- app/locale/pt-BR.coffee
+++ app/locale/pt-BR.coffee
@@ -575,7 +575,7 @@
     ready_to_share: "Pronto para publicar seu projeto?"
     click_publish: "Clique em \"Publicar\" para que apareça na galeria da turma, depois confira o que seus colegas construíram! Você pode voltar e continuar trabalhando neste projeto. Qualquer outra alteração será salva automaticamente e compartilhada com seus colegas."
     already_published_prefix: "Suas mudanças foram publicadas na galeria da turma."
-    already_published_suffix: Continue experimentando e tornando este projeto ainda melhor, ou veja o que o resto da sua turma construiu! Suas mudanças serão automaticamente salvas e compartilhadas com seus colegas."
+    already_published_suffix: "Continue experimentando e tornando este projeto ainda melhor, ou veja o que o resto da sua turma construiu! Suas mudanças serão automaticamente salvas e compartilhadas com seus colegas."
     view_gallery: "Visualizar Galeria"
     project_published_noty: "Seu nível foi o publicado!"
     keep_editing: "Continue Editando"
@@ -652,7 +652,11 @@
   subscribe:
     premium_already_subscribed: "Você já se inscreveu como Premium!"
     subscribe_modal_title: "CodeCombat Premium"
+<<<<<<< HEAD
     comparison_blurb: "Seja um programador. Faça a inscrição <b>Premium</b> hoje!" # {change}
+=======
+    comparison_blurb: "Seja um programador - faça a inscrição Premium hoje!"
+>>>>>>> 8a67450e2667dde8686f926d085e308fe98d9323
     premium_pricing_prefix: "Seja Premium por apenas"
     premium_pricing_suffix: "e se torne um programador."
     premium: "Premium" # Make sure the following feature translations don't go onto two lines


Run copy-i18n-tags (#5173)

Run copy-i18n-tags (#5173)

--- app/locale/rot13.coffee
+++ app/locale/rot13.coffee
@@ -75,7 +75,7 @@
     quotes_quote3: "PbqrPbzong unf orra gur zbfg orarsvpvny sbe grnpuvat zl fghqragf erny-yvsr pbqvat pncnovyvgvrf. Zl uhfonaq vf n fbsgjner ratvarre naq ur unf grfgrq bhg nyy bs zl cebtenzf. Ur chg guvf nf uvf gbc pubvpr."
     quotes_quote4: "Gur srrqonpx … unf orra fb cbfvgvir gung jr ner fgehpghevat n pbzchgre fpvrapr pynff nebhaq PbqrPbzong. Gur cebtenz ernyyl ratntrf gur fghqragf jvgu n tnzvat fglyr cyngsbez gung vf ragregnvavat naq vafgehpgvbany ng gur fnzr gvzr. Xrrc hc gur tbbq jbex, PbqrPbzong!"
     see_example: "Frr rknzcyr"
-    slogan: "Gur zbfg ratntvat jnl gb yrnea pbzchgre fpvrapr" # {punatr}
+    slogan: "Gur zbfg ratntvat jnl gb yrnea pbzchgre fpvrapr"
     teach_cs1_free: "Grnpu PF1 Serr"
     teachers_love_codecombat_title: "Grnpuref Ybir PbqrPbzong"
     teachers_love_codecombat_blurb1: "Ercbeg gung gurve fghqragf rawbl hfvat PbqrPbzong gb yrnea ubj gb pbqr"


Event cares about mediator.playWithSound now

--- app/models/Event.coffee
+++ app/models/Event.coffee
@@ -29,8 +29,9 @@

         @level.addEntity yeti

-        mediator.soundManager.stopAll config =
-          themeSound: true
-          backgroundSounds: true
+        if mediator.playWithSounds
+          mediator.soundManager.stopAll config =
+            themeSound: true
+            backgroundSounds: true

-        mediator.soundManager.playSound @level.manifest.sounds.sounds[0], 1, true
+          mediator.soundManager.playSound @level.manifest.sounds.sounds[0], 1, true


make it so you can set the shape of the universe as an option

--- demo.coffee
+++ demo.coffee
@@ -1,5 +1,6 @@
 # code to wire the evolution code to the demo page
 initializationParameters =
+  torus: false
   cell: [ 20, 10, 50, 'scale at which universe is indexed; smaller numbers means more speed and more memory consumption' ]
   pause: [ 10, 0, 1000, 'minimum pause between ticks' ]
   maxDistance: [ 60, 30, 100, 'maximum distance an organism can see' ]
@@ -102,6 +103,12 @@
     object[label] = s.value
     sp.innerHTML = s.value
 makeCheckbox = (label, obj, parent) ->
+  value = obj[label]
+  s = create 'input'
+  s.type = 'checkbox'
+  s.checked = value
+  parent.appendChild s
+  s.onchange = -> obj[label] = s.checked
 makeSlider = (label, object, parent) ->
   values = object[label]
   s = create 'input'


Add beta email flag to user settings page

--- app/pages/settings.cjsx
+++ app/pages/settings.cjsx
@@ -142,6 +142,13 @@
                 Get general Zooniverse email updates
               </label>
             </AutoSave>
+            <br />
+            <AutoSave resource={@props.user}>
+              <label>
+                <input type="checkbox" name="beta_email_communication" checked={@props.user.beta_email_communication} onChange={handleInputChange.bind @props.user} />{' '}
+                Get beta project email updates
+              </label>
+            </AutoSave>
           </p>
         </div>
       </div>


:fire: Remove line node `style.top`

:fire: Remove line node `style.top`

--- src/lines-tile-component.coffee
+++ src/lines-tile-component.coffee
@@ -308,10 +308,6 @@
           lineNode.classList.add(decorationClass)

     oldLineState.decorationClasses = newLineState.decorationClasses
-
-    if newLineState.top isnt oldLineState.top
-      lineNode.style.top = newLineState.top + 'px'
-      oldLineState.top = newLineState.top

     if newLineState.screenRow isnt oldLineState.screenRow
       lineNode.dataset.screenRow = newLineState.screenRow


model_postgres: safety checks

model_postgres: safety checks

--- src/local/model_postgres.coffee
+++ src/local/model_postgres.coffee
@@ -217,6 +217,11 @@
     ##

     getSubscription: (node, user, cb) ->
+        unless node
+            return cb(new Error("No node"))
+        unless user
+            return cb(new Error("No user"))
+
         db = @db
         async.waterfall [(cb2) ->
             db.query "SELECT subscription FROM subscriptions WHERE node=$1 AND user=$2", [ node, user ], cb2
@@ -225,6 +230,11 @@
         ], cb

     setSubscription: (node, user, listener, subscription, cb) ->
+        unless node
+            return cb(new Error("No node"))
+        unless user
+            return cb(new Error("No user"))
+
         db = @db
         toDelete = not subscription or subscription == "none"
         async.waterfall [ @validateNode(node)
@@ -264,6 +274,9 @@
             cb err

     getSubscribers: (node, cb) ->
+        unless node
+            return cb(new Error("No node"))
+
         db = @db
         async.waterfall [(cb2) ->
             db.query "SELECT \"user\", subscription FROM subscriptions WHERE node=$1 ORDER BY updated DESC", [ node ], cb2
@@ -312,11 +325,21 @@
     ##

     getAffiliation: (node, user, cb) ->
+        unless node
+            return cb(new Error("No node"))
+        unless user
+            return cb(new Error("No user"))
+
         @db.query "SELECT affiliation FROM affiliations WHERE node=$1 AND \"user\"=$2 ORDER BY updated DESC", [ node, user ], cb2
         , (err, res) ->
             cb err, (res?.rows?[0]?.affiliation or "none")

     setAffiliation: (node, user, affiliation, cb) ->
+        unless node
+            return cb(new Error("No node"))
+        unless user
+            return cb(new Error("No user"))
+
         db = @db
         async.waterfall [ @validateNode(node)
         , (cb2) ->
@@ -336,6 +359,9 @@
             cb err

     getAffiliations: (user, cb) ->
+        unless user
+            return cb(new Error("No user"))
+
         db = @db
         async.waterfall [(cb2) ->
             db.query "SELECT node, affiliation FROM affiliations WHERE \"user\"=$1 ORDER BY updated DESC", [ user ], cb2


[IMPAC-598] Fix bug with update then delete threshold

--- src/services/kpis/kpis.svc.coffee
+++ src/services/kpis/kpis.svc.coffee
@@ -341,7 +341,7 @@
               ImpacEvents.notifyCallbacks(IMPAC_EVENTS.addOrRemoveAlerts)
               angular.extend(kpi, success.data)
               _self.buildKpiWatchables(kpi)
-              _self.show(kpi) if showKpi
+              if showKpi then _self.show(kpi) else kpi
             (err) ->
               $log.error("Impac! - KpisSvc: Unable to update KPI #{kpi.id}", err)
               $q.reject(err)


Only retry fetching workflow if selected_workflow exists (#3566)

--- app/pages/project/index.cjsx
+++ app/pages/project/index.cjsx
@@ -102,7 +102,7 @@
       else
         @getAllWorkflows(nextProps.project)

-    if nextProps.preferences?.preferences? and @state.selectedWorkflow?
+    if nextProps.preferences?.preferences?.selected_workflow? and @state.selectedWorkflow?
       if nextProps.preferences?.preferences.selected_workflow isnt @state.selectedWorkflow.id
         @getSelectedWorkflow(nextProps.project, nextProps.preferences)

@@ -404,15 +404,12 @@

   fetchProjectData: (ownerName, projectName, user) ->
     @listenToPreferences null
-    @setState
-      background: null
-      error: null
-      loading: true
-      owner: null
-      pages: null
-      preferences: null
-      projectAvatar: null
-      projectRoles: null
+    @setState({
+      error: null,
+      loading: true,
+      preferences: null,
+    })
+

     slug = ownerName + '/' + projectName


update preferences.selected_workflow with query

--- app/pages/project/index.cjsx
+++ app/pages/project/index.cjsx
@@ -134,6 +134,8 @@
     # if none of those are set, select random workflow
     if @props.location.query?.workflow? and 'allow workflow query' in @props.project.experimental_tools
       preferredWorkflowID = @props.location.query.workflow
+      unless preferences?.preferences.selected_workflow is preferredWorkflowID
+        @props.onChangePreferences 'preferences.selected_workflow', preferredWorkflowID
     else if preferences?.preferences.selected_workflow?
       preferredWorkflowID = preferences?.preferences.selected_workflow
     else if preferences?.settings?.workflow_id?


Deprecate all the views

Deprecate all the views

--- exports/atom.coffee
+++ exports/atom.coffee
@@ -15,29 +15,77 @@
 # The following classes can't be used from a Task handler and should therefore
 # only be exported when not running as a child node process
 unless process.env.ATOM_SHELL_INTERNAL_RUN_AS_NODE
-  module.exports.TextEditorView = require '../src/text-editor-view'
-  module.exports.ScrollView = require '../src/scroll-view'
-  module.exports.SelectListView = require '../src/select-list-view'
   module.exports.Task = require '../src/task'
   module.exports.WorkspaceView = require '../src/workspace-view'
   module.exports.Workspace = require '../src/workspace'

   {$, $$, $$$, View} = require '../src/space-pen-extensions'
   Object.defineProperty module.exports, '$', get: ->
-    deprecate "Please require `jquery` instead: `$ = require 'jquery'`. Add `\"jquery\": \"^2\"` to your package dependencies."
+    deprecate """
+      Please require `jquery` instead:
+        `$ = require 'jquery'`
+      Add `"jquery": "^2"` to your package dependencies.
+    """
     $

   Object.defineProperty module.exports, '$$', get: ->
-    deprecate "Please require `space-pen` instead: `{$$} = require 'space-pen'`. Add `\"space-pen\": \"^3\"` to your package dependencies."
+    deprecate """
+      Please require `space-pen` instead:
+        `{$$} = require 'space-pen'`
+      Add `"space-pen": "^3"` to your package dependencies.
+    """
     $$

   Object.defineProperty module.exports, '$$$', get: ->
-    deprecate "Please require `space-pen` instead: `{$$$} = require 'space-pen'`. Add `\"space-pen\": \"^3\"` to your package dependencies."
+    deprecate """
+      Please require `space-pen` instead:
+        `{$$$} = require 'space-pen'`
+      Add `"space-pen": "^3"` to your package dependencies.
+    """
     $$$

   Object.defineProperty module.exports, 'View', get: ->
-    deprecate "Please require `space-pen` instead: `{View} = require 'space-pen'`. Add `\"space-pen\": \"^3\"` to your package dependencies."
+    deprecate """
+      Please require `space-pen` instead:
+        `{View} = require 'space-pen'`
+      Add `"space-pen": "^3"` to your package dependencies.
+    """
     View
+
+
+  Object.defineProperty module.exports, 'EditorView', get: ->
+    deprecate """
+      Please require `TextEditorView` from `atom-space-pen-view` instead:
+        `{TextEditorView} = require 'atom-space-pen-views'`
+      Add `"atom-space-pen-views": "^0"` to your package dependencies.
+    """
+    require '../src/text-editor-view'
+
+  Object.defineProperty module.exports, 'TextEditorView', get: ->
+    deprecate """
+      Please require `TextEditorView` from `atom-space-pen-view` instead:
+        `{TextEditorView} = require 'atom-space-pen-views'`
+      Add `"atom-space-pen-views": "^0"` to your package dependencies.
+    """
+    require '../src/text-editor-view'
+
+  Object.defineProperty module.exports, 'ScrollView', get: ->
+    deprecate """
+      Please require `ScrollView` from `atom-space-pen-view` instead:
+        `{ScrollView} = require 'atom-space-pen-views'`
+      Note that the API has changed slightly! Please read the docs at https://github.com/atom/atom-space-pen-views
+      Add `"atom-space-pen-views": "^0"` to your package dependencies.
+    """
+    require '../src/scroll-view'
+
+  Object.defineProperty module.exports, 'SelectListView', get: ->
+    deprecate """
+      Please require `SelectListView` from `atom-space-pen-view` instead:
+        `{SelectListView} = require 'atom-space-pen-views'`
+      Note that the API has changed slightly! Please read the docs at https://github.com/atom/atom-space-pen-views
+      Add `"atom-space-pen-views": "^0"` to your package dependencies.
+    """
+    require '../src/select-list-view'

   Object.defineProperty module.exports, 'React', get: ->
     deprecate "Please require `react-atom-fork` instead: `React = require 'react-atom-fork'`. Add `\"react-atom-fork\": \"^0.11\"` to your package dependencies."
@@ -50,7 +98,3 @@
 Object.defineProperty module.exports, 'Git', get: ->
   deprecate "Please require `GitRepository` instead of `Git`: `{GitRepository} = require 'atom'`"
   module.exports.GitRepository
-
-Object.defineProperty module.exports, 'EditorView', get: ->
-  deprecate "Please require `TextEditorView` instead of `EditorView`: `{TextEditorView} = require 'atom'`"
-  module.exports.TextEditorView


bundle: add the base implementation of #debit

--- workers/social/lib/social/models/bundle/index.coffee
+++ workers/social/lib/social/models/bundle/index.coffee
@@ -7,6 +7,7 @@
   {dash} = require 'bongo'

   {extend} = require 'underscore'
+
   # @create = ()

   @setLimits = (limits) ->
@@ -47,8 +48,6 @@

           limitOptions = limits[limitName]

-          limitOptions = { unit: limit }  if 'string' is typeof limitOptions
-
           limitOptions = extend { title: limitName }, defaultOptions, limitOptions

           limit = new JLimit limitOptions
@@ -61,3 +60,17 @@
         fin = queue.fin.bind queue

         return
+
+  debit: (debits, callback=(->)) ->
+    @fetchLimits (err, limits) ->
+      return callback err  if err
+
+      queue = limits.map (limit) -> ->
+        {title} = limit
+        console.log title
+        if title of debits and limit.getValue() >= debits[title]
+          limit.update { $inc: usage: debits[title] }, fin
+
+      dash queue, callback
+
+      fin = queue.fin.bind queue


compute server: delay update_states during startup and less parallelization

--- src/smc-hub/compute-server.coffee
+++ src/smc-hub/compute-server.coffee
@@ -29,6 +29,8 @@
 CONF = '/projects/conf'
 SQLITE_FILE = undefined
 DEV = false    # if true, in special single-process dev mode, where this code is being run directly by the hub.
+
+START_TIME = new Date().getTime() # milliseconds

 # IMPORTANT: see schema.coffee for some important information about the project states.
 STATES = require('smc-util/schema').COMPUTE_STATES
@@ -1134,9 +1136,12 @@
                                 cb(err)
                             else
                                 project.state(update:true, cb:cb)
-            async.mapLimit(projects, 20, f, cb)
+            async.mapLimit(projects, 8, f, cb)
         ], (err) ->
-            setTimeout(update_states, 2*60*1000)
+            # slow down during the first 10 minutes after startup
+            startup = ((new Date().getTime()) - START_TIME) > 10*60*1000
+            delay_s = if startup then 10 else 2
+            setTimeout(update_states, delay_s * 60 * 1000)
             cb?(err)
         )


course editor: show whether each project is on a members-only server or free host

--- src/smc-webapp/editor_course.cjsx
+++ src/smc-webapp/editor_course.cjsx
@@ -1244,6 +1244,18 @@
         else
             return (has never used project)

+    render_hosting: ->
+        student_project_id = @props.student.get('project_id')
+        if student_project_id
+            if @props.redux.getStore('projects').get_total_project_quotas(student_project_id).member_host
+                <Tip placement='left' title={<Icon name='check'/> Members-only hosting} tip='Projects is on a members-only server, which is much more robust and has priority support.'>
+                    <Icon name='check'/> Members-only server
+                </Tip>
+            else
+                <Tip placement='left' title={<Icon name='exclamation-triangle'/> Free hosting} tip='Project is hosted on a free server, so it may be overloaded and will be rebooted frequently.  Please upgrade in course settings.'>
+                     <Icon name='exclamation-triangle'/> Free server
+                </Tip>
+
     render_project : ->
         # first check if the project is currently being created
         create = @props.student.get("create_project")
@@ -1373,8 +1385,11 @@
                     {@render_student_email()}

             </Col>
-            <Col md=7 style={paddingTop:'10px'}>
+            <Col md=4 style={paddingTop:'10px'}>
                 {@render_last_active()}
+            </Col>
+            <Col md=3 style={paddingTop:'10px'}>
+                {@render_hosting()}
             </Col>
         </Row>

@@ -2868,7 +2883,7 @@
             {if @state.upgrade_quotas then @render_upgrade_quotas() else @render_upgrade_quotas_button()}

-
You may add additional quota upgrades to all of the projects in this course, augmenting what is provided for free and what students may have purchased.  Your contributions will be split evenly between all non-deleted student projects.

+
You may add additional quota upgrades to all of the projects in this course, augmenting what is provided for free and what students may have purchased.  Your contributions will be split evenly between student projects.

If you add new students, currently you must re-open the quota panel and re-allocate quota so that newly added projects get additional upgrades; alternatively, you may open any project directly and edit its quotas in project settings.

@@ -2981,7 +2996,7 @@
         if @state.students_pay
             <Icon name="check"/> <Space />{@render_require_students_pay_desc(@state.students_pay_when)}
         else
-            You may require that all students in the course pay a one-time $9 fee to move their projects to members only hosts and enable network access, for four months.
+            You may require that all students in the course pay a one-time $9 fee to move their projects to members only hosts and enable network access, for four months.  This is optional, but will ensure that your students have a better experience and receive priority support.

     render_require_students_pay: ->


"Check all" button in file browser doesn't do anything: https://github.com/sagemathinc/smc/issues/326

--- src/smc-webapp/project_files.cjsx
+++ src/smc-webapp/project_files.cjsx
@@ -615,7 +615,7 @@
             @setState(select_entire_directory : 'hidden')

     check_all_click_handler : ->
-        if @props.checked_files.size is 0
+        if @props.checked_files.size == 0
             files_on_page = @props.listing[@props.page_size * @props.page_number...@props.page_size * (@props.page_number + 1)]
             @props.actions.set_file_list_checked(misc.path_to_file(@props.current_path, file.name) for file in files_on_page)

@@ -1424,7 +1424,7 @@
                 checked_files = {@props.checked_files}
                 file_action   = {@props.file_action}
                 page_number   = {@props.page_number}
-                page_size     = {@props.file_listing_page_size}
+                page_size     = {@file_listing_page_size()}
                 public_view   = {public_view}
                 current_path  = {@props.current_path}
                 listing       = {listing}
@@ -1488,7 +1488,7 @@
         else if listing?
             <FileListing
                 listing       = {listing}
-                page_size     = {@props.file_listing_page_size}
+                page_size     = {@file_listing_page_size()}
                 page_number   = {@props.page_number}
                 file_map      = {file_map}
                 checked_files = {@props.checked_files}
@@ -1505,6 +1505,9 @@
             <ProjectState state={project_state} />
         </div>

+    file_listing_page_size: ->
+        return @props.other_settings?.get('page_size') ? 50
+
     render : ->
         if not @props.checked_files?  # hasn't loaded/initialized at all
             return <Loading />
@@ -1518,7 +1521,7 @@

         {listing, error, file_map} = @props.redux.getProjectStore(@props.project_id)?.get_displayed_listing()

-        file_listing_page_size = @props.other_settings?.get('page_size') ? 50
+        file_listing_page_size= @file_listing_page_size()
         if listing?
             {start_index, end_index} = pager_range(file_listing_page_size, @props.page_number)
             visible_listing = listing[start_index...end_index]


Handles all jumping when scrolled to the right!

- Cleanup.
- Fixes #4.

--- lib/jumpy-view.coffee
+++ lib/jumpy-view.coffee
@@ -64,25 +64,18 @@
       nearestCursor =
           left: nearestMultiple(labelLocation.left, cursor.clientWidth)
           top: nearestMultiple(labelLocation.bottom - labelLocation.height, cursor.clientHeight)
-      console.log nearestCursor.left, nearestCursor.top, cursor.clientHeight, labelLocation

       lines = atom.workspaceView.find('.lines')
       offsetTop = lines.get(0).offsetTop
-      #offsetLeft = lines.get(0).offsetLeft
-      offsetLeft = 0 # TODO: this needs to be replaced with scroll to the right info.
+      offsetLeft = $('.scroll-view').scrollLeft()
       scrollViewOffset = $('.editor .scroll-view').offset()
       for line, lineIndex in @pixels
           line = _.compact line
           for char, charIndex in line
               isAtLeft = (nearestCursor.left ==
-                  nearestMultiple(char.left + scrollViewOffset.left + offsetLeft, cursor.clientWidth))
+                  nearestMultiple(char.left + scrollViewOffset.left - offsetLeft, cursor.clientWidth))
               isAtTop = (nearestCursor.top ==
                   nearestMultiple(char.top + scrollViewOffset.top + offsetTop, cursor.clientHeight))
-
-              if lineIndex == @pixels.length - 2 && charIndex == 0
-                  # TODO: handle changing y coord after 2nd run...found class="cc aa"!
-                  # TODO: missing offset
-                  console.log "Last one:", lineIndex, charIndex, char.left, isAtLeft, isAtTop

               if isAtLeft && isAtTop
                   return [lineIndex, charIndex]


Added transfrom feature

Added transfrom feature

--- lib/both.coffee
+++ lib/both.coffee
@@ -1,6 +1,6 @@
 class AntiSearchSourceClient
   constructor: (@_searchConfig, @_subscriptionContext = Meteor) ->
-    @_searchConfig.limit = @_searchConfig.limit || 21
+    @_searchConfig.limit = @_searchConfig.limit || 0

     @_collection = Mongo.Collection.get(@_searchConfig.collection)
     @_stateFlag = new ReactiveVar(false);
@@ -11,6 +11,17 @@
     if err then console.log('Error while searching', err)

   _stateChanged: -> @_stateFlag.set(!@_stateFlag.get())
+
+  _createTransformFn: (usersTransform) ->
+    transformFn = (document) =>
+      searchStr = @_searchConfig.searchString
+      if searchStr
+        searchStrRegExp = new RegExp(searchStr, 'ig')
+        @_searchConfig.fields.forEach (fieldName) =>
+          document[fieldName] = usersTransform(document[fieldName], searchStrRegExp)
+      return document
+
+    if _.isFunction usersTransform then transformFn else null

   _updateQuery: ->
     @_stateChanged()
@@ -26,34 +37,29 @@
       @_searchSubscribtion = @_subscriptionContext.subscribe AntiSearchSource._publisherName, @_searchConfig, @_onSubscriptionReady

   setMongoQuery: (newMongoQuery) ->
-    if _.isEqual newMongoQuery, @_searchConfig.mongoQuery
-      return
-    else
+    unless _.isEqual newMongoQuery, @_searchConfig.mongoQuery
       @_searchConfig.mongoQuery = newMongoQuery
-
       @_updateQuery()

 # May be used for infinite scroll or something like that
   setLimit: (newLimit) ->
-    if @_searchConfig.limit == newLimit
-      return
-    else
-    @_searchConfig.limit = newLimit
-
-    @_updateQuery()
+    unless @_searchConfig.limit == newLimit
+      @_searchConfig.limit = newLimit
+      @_updateQuery()

   incrementLimit: (step = 10) ->
     @_searchConfig.limit += step
-
     @_updateQuery()
-

 # Reactive data source
   searchResult: (options = {}) ->
     @_stateFlag.get()
     if @_searchConfig.searchMode is 'local' or @_searchSubscribtion and @_searchSubscribtion.ready()
       query = AntiSearchSource._buildSearchQuery(@_searchConfig)
-      _.extend options, {limit: @_searchConfig.limit}
+      _.extend options,
+        limit: @_searchConfig.limit
+        transform: @_createTransformFn(options.transform)
+
       return @_collection.find(query, options)

 # Cancels subscription for search data
@@ -97,6 +103,4 @@
   queryTransform: (collectionName, transformCallback) ->
     @_transforms[collectionName] = transformCallback

-  create: (config) -> new AntiSearchSourceClient(config)
-
-
+  create: (config) -> new AntiSearchSourceClient(config)


Fix bad regex format

Fix bad regex format

--- app/src/storage/store.coffee
+++ app/src/storage/store.coffee
@@ -10,7 +10,8 @@
   constructor: (name, keySeparator = '.') ->
     @_name = name
     @_keySeparator = keySeparator
-    @_nameRegex = new RegExp("^#{@_name}\\.")
+    regexSeparator = if keySeparator.match(/\./) then "\\" + keySeparator else keySeparator
+    @_nameRegex = new RegExp("^#{@_name}#{regexSeparator}")
     @_substores = {}

   # Gets one or more items from storage.


fix urls in spec

fix urls in spec

--- shared/test/model/notifications.spec.coffee
+++ shared/test/model/notifications.spec.coffee
@@ -18,11 +18,11 @@
     URLs.reset()

   it 'polls when URL is set', ->
-    URLs.update(base_accounts_url: 'http://localhost:2999')
+    URLs.update(accounts_api_url: 'http://localhost:2999')
     Notifications.startPolling(@windowImpl)
     expect(Poller::poll).to.have.callCount(1)
     expect(Poller::setUrl.lastCall.args).to.deep.equal(['http://localhost:2999/api/user'])
-    URLs.update(tutor_notices_url: 'http://localhost:3001/api/notifications')
+    URLs.update(tutor_api_url: 'http://localhost:3001/api/notifications')
     Notifications.startPolling(@windowImpl)
     expect(Poller::poll).to.have.callCount(2)


Fixed #78 FileNotFoundException when no user namespace defined.

Fixed "Cannot resolve :after symbol user/reset" when not defined reset function in user namespace.
Changed warning messages

--- lib/proto-repl.coffee
+++ lib/proto-repl.coffee
@@ -445,22 +445,27 @@

   refreshNamespacesCommand:
     "(do
-      (require 'user)
-      (if (find-ns 'user)
-        (let [r 'user/reset
-              result (cond
-                      (find-var r)
-                      ((resolve r))
-
-                      (find-ns 'clojure.tools.namespace.repl)
-                      (eval `(clojure.tools.namespace.repl/refresh :after '~r))
-
-                      :else
-                      (println \"clojure.tools.namespace.repl not available. Add as a dependency and require in user.clj.\"))]
-          (when (isa? (type result) Exception)
-            (println (.getMessage result)))
-          result)
-       (println \"No user namespace defined to allow refreshing. Define a user namespace.\")))"
+      (try
+        (require 'user)
+        (catch java.io.FileNotFoundException e
+          (println \"No user namespace defined.\")))
+      (let [r 'user/reset
+            result (cond
+                     (find-var r)
+                     ((resolve r))
+
+                     (find-ns 'clojure.tools.namespace.repl)
+                     (do
+                       (println (str \"Using clojure.tools.namespace.repl/refresh to reload\\n\"
+                                     \"You can use your own refresh function, just define reset function in user namespace\\n\"
+                                     \"See this https://github.com/clojure/tools.namespace#reloading-code-motivation for why you should use it\"))
+                       (eval `(clojure.tools.namespace.repl/refresh)))
+
+                     :else
+                     (println \"clojure.tools.namespace.repl not available. Add as a dependency and require in user.clj.\"))]
+        (when (isa? (type result) Exception)
+          (println (.getMessage result)))
+        result))"

   refreshResultHandler: (callback, result)->
     # Value will contain an exception if it's not valid otherwise it will be nil


don't need this

don't need this

--- lib/atom-ternjs-helper.coffee
+++ lib/atom-ternjs-helper.coffee
@@ -5,7 +5,6 @@
 class Helper

   projectRoot: null
-  definition: null
   ternProjectFileContent: '{\n
     \ "libs": [\n
     \ \ \ "browser",\n


clean up advance timer setup

clean up advance timer setup

--- app/views/js/app_view.coffee
+++ app/views/js/app_view.coffee
@@ -31,9 +31,7 @@
     @setupAutoAdvance()

   setupAutoAdvance: ->
-    @advanceTimer = setInterval =>
-      @advance()
-    , 30000
+    @advanceTimer = setInterval _.bind(@advance, this), 30000

   timeWindowsChanged: ->
     @timeSlice.set(timeWindows: @configuration.get('timewindows'))


Fix tool focusing to listen for correct attribute

--- app/views/tool_window.coffee
+++ app/views/tool_window.coffee
@@ -20,7 +20,7 @@
     @titleBar = new @windowTitleBarView {model: @model}

     @model.on
-      'change:id' : @updateWindowId
+      'change:_id' : @updateWindowId
       'destroy': @removeWindow
       'change:zindex': @setZindex
       'change:left change:top': @setPosition


more import/export fixes

more import/export fixes

--- lib/closure-conversion.coffee
+++ lib/closure-conversion.coffee
@@ -1827,7 +1827,11 @@

         visitImportDeclaration: (n) ->
                 assign = { type: AssignmentExpression, operator: "=" }
-                assign.right = create_intrinsic(moduleGet_id, [n.source])
+                assign.right =
+                        type:     MemberExpression
+                        object:   create_intrinsic(moduleGet_id, [n.source])
+                        property: create_identifier "default"
+                        computed: false
                 assign.left = create_lhs(n.specifiers)

                 { type: ExpressionStatement, expression: assign, loc: n.loc }
@@ -1854,16 +1858,37 @@
                                 }],
                                 kind: "let"
                         }
-
-
+
+                # export function foo () { ... }
                 if n.declaration.type is FunctionDeclaration
                         @exports.push n.declaration.id
                         return n.declaration
-
+
+                # export let foo = bar;
                 if n.declaration.type is VariableDeclaration
                         @exports.push decl.id for decl in n.declaration.declarations
                         return n.declaration
-
+
+                # export foo = bar;
+                if n.declaration.type is VariableDeclarator
+                        @exports.push n.declaration.id
+                        return n.declaration
+
+                # export default = ...;
+                #
+                if n.default
+                        default_id = create_identifier "default"
+                        @exports.push default_id
+                        return {
+                                type: VariableDeclaration,
+                                declarations: [{
+                                        type: VariableDeclarator,
+                                        id:   default_id
+                                        init: n.declaration
+                                }],
+                                kind: "var"
+                        }
+
                 throw new Error("Unsupported type of export declaration #{n.declaration.type}")


fix pre-boot stdout/err

fix pre-boot stdout/err

--- lib/connection/process.coffee
+++ lib/connection/process.coffee
@@ -132,17 +132,6 @@
       else client.stderr err

   init: (conn) ->
-    conn.proc.on 'exit', (code, signal) =>
-      client.stderr "Julia has stopped"
-      if not @useWrapper then client.stderr ": #{code}, #{signal}"
-      client.cancelBoot()
-    conn.proc.stdout.on 'data', (data) => client.stdout data.toString()
-    conn.proc.stderr.on 'data', (data) =>
-      text = data.toString()
-      if text.startsWith 'juno-err'
-        @bootErr text
-        return
-      client.stderr text
     conn.interrupt = => @interrupt conn.proc
     conn.kill = => @kill conn.proc
     conn.stdin = (data) -> conn.proc.stdin.write data
@@ -153,6 +142,18 @@
     @checkPath @jlpath()
       .then =>
         @spawnJulia port, (proc) =>
+          proc.on 'exit', (code, signal) =>
+            client.stderr "Julia has stopped"
+            if not @useWrapper then client.stderr ": #{code}, #{signal}"
+            client.cancelBoot()
+          proc.stdout.on 'data', (data) => client.stdout data.toString()
+          proc.stderr.on 'data', (data) =>
+            text = data.toString()
+            if text.startsWith 'juno-err'
+              @bootErr text
+              return
+            client.stderr text
+
           tcp.next().then (conn) =>
             conn.proc = proc
             @init conn


Improve transform of avatar to convert all images into jpeg

--- lib/file.coffee
+++ lib/file.coffee
@@ -1,13 +1,13 @@
 if Meteor.isServer
 	transformWrite = (file, readStream, writeStream) ->
-		RocketFile.gm(readStream, file.fileName).resize(10, 10).stream().pipe(writeStream)
+		RocketFile.gm(readStream, file.fileName).background('#ffffff').resize(200, 200).gravity('Center').extent(200, 200).stream('jpeg').pipe(writeStream)

-	@RocketFileInstance = new RocketFile.GridFS
-		name: 'avatars'
+	# @RocketFileInstance = new RocketFile.GridFS
+	# 	name: 'avatars'
+	# 	transformWrite: transformWrite
+
+	@RocketFileInstance = new RocketFile.FileSystem
 		transformWrite: transformWrite
-
-	# @RocketFileInstance = new RocketFile.FileSystem
-		# transformWrite: transformWrite

 	HTTP.methods
 		'/avatar/:username':


Make forFile the only interface for creating an attachment with a file

--- src/trix/models/attachment.coffee
+++ src/trix/models/attachment.coffee
@@ -7,13 +7,12 @@
     @attachments[id]

   @forFile: (file) ->
-    attachment = new this {}, file
+    attachment = new this { contentType: file.type }
+    attachment.file = file
     attachment.dispatchAdd()

-  constructor: (@attributes = {}, @file) ->
+  constructor: (@attributes = {}) ->
     @fileHandler = @constructor.delegate ? {}
-    if @file
-      @attributes.contentType ?= @file.type

   save: ->
     return this if @id


Tweak replace pattern for final cleanup.

--- lib/docxconv/converter.coffee
+++ lib/docxconv/converter.coffee
@@ -70,7 +70,9 @@
   return callback("[!] Final cleanup: no HTML to cleanup.") if isEmpty(html)

   try
-    html = html.replace(//g, '').replace(/\\\s*\n/g, '')
+    html = html.toString().replace(/\\\s*\n/g, '')
+      .replace(/|\x{E2808B}| /g, '')
+
     $ = cheerio.load(html)
     $('*:empty').remove()
     result = $.html()


Make some hardcoding more visible.

Make some hardcoding more visible.

--- src/views/position-details.coffee
+++ src/views/position-details.coffee
@@ -16,6 +16,24 @@
     RouteView
 ) ->
     UNIT_INCLUDE_FIELDS = 'name,root_services,location,street_address'
+    SORTED_DIVISIONS = [
+        'postcode_area',
+        'neighborhood',
+        'health_station_district',
+        'maternity_clinic_district',
+        'income_support_district',
+        'lower_comprehensive_school_district_fi',
+        'upper_comprehensive_school_district_fi',
+        'lower_comprehensive_school_district_sv',
+        'upper_comprehensive_school_district_sv',
+        'rescue_area',
+        'rescue_district',
+        'rescue_sub_district',
+    ]
+    # the following ids represent
+    # rescue related service points
+    # such as emergency shelters
+    EMERGENCY_UNIT_SERVICES = [26214, 26210, 26208]

     class PositionDetailsView extends base.SMLayout
         type: 'position'
@@ -39,20 +57,6 @@
             @route = options.route
             @parent = options.parent
             @routingParameters = options.routingParameters
-            @sortedDivisions = [
-                'postcode_area',
-                'neighborhood',
-                'health_station_district',
-                'maternity_clinic_district',
-                'income_support_district',
-                'lower_comprehensive_school_district_fi',
-                'upper_comprehensive_school_district_fi',
-                'lower_comprehensive_school_district_sv',
-                'upper_comprehensive_school_district_sv',
-                'rescue_area',
-                'rescue_district',
-                'rescue_sub_district',
-                ]
             @hiddenDivisions =
                 emergency_care_district: true

@@ -61,8 +65,8 @@
                 @fetchDivisions().done =>
                     @render()
             @divList.comparator = (a, b) =>
-                indexA = _.indexOf @sortedDivisions, a.get('type')
-                indexB = _.indexOf @sortedDivisions, b.get('type')
+                indexA = _.indexOf SORTED_DIVISIONS, a.get('type')
+                indexB = _.indexOf SORTED_DIVISIONS, b.get('type')
                 if indexA < indexB then return -1
                 if indexB < indexA then return 1
                 return 0
@@ -71,10 +75,7 @@
             deferreds = []
             @rescueUnits = {}
             deferreds.push @fetchDivisions(coords)
-            # the following ids represent
-            # rescue related service points
-            # such as emergency shelters
-            for serviceId in [26214, 26210, 26208]
+            for serviceId in EMERGENCY_UNIT_SERVICES
                 coll = new models.UnitList()
                 @rescueUnits[serviceId] = coll
                 deferreds.push @fetchRescueUnits(coll, serviceId, coords)
@@ -99,7 +100,7 @@
                     lon: coords[0]
                     lat: coords[1]
                     unit_include: UNIT_INCLUDE_FIELDS
-                    type: (_.union @sortedDivisions, ['emergency_care_district']).join(',')
+                    type: (_.union SORTED_DIVISIONS, ['emergency_care_district']).join(',')
                     geometry: 'true'
                 reset: true
         serializeData: ->


refactor(): replace brittle reliance on error message substring matching with an exact error-code and argument(s) comparison.

--- lib/hint.coffee
+++ lib/hint.coffee
@@ -2,6 +2,24 @@
 fs              = require( 'fs' )
 jshint          = require( 'jshint' ).JSHINT
 _               = require( 'underscore' )
+
+
+##  Adapted from https://github.com/jshint/jshint/blob/1553e3af6d2453eb52507a2c97971094897794d3/src/jshint.js#L158
+##
+supplantRe      = /\{([^{}]*)\}/g
+supplant        = ( str, args ) ->
+
+    return str.replace( supplantRe, ( matched, argName ) ->
+
+        argValue = args[ argName ]
+
+        return switch ( typeof argValue )
+            when 'number', 'string'
+                argValue
+            else
+                matched
+    )
+

 defaultOptions  = [

@@ -16,13 +34,47 @@
     'multistr'
 ]

-errorsToSkip    = [
-    'Did you mean to return a conditional instead of an assignment?'
-    'Confusing use of \'!\'.'
-    'Wrap the /regexp/ literal in parens to disambiguate the slash operator.'
-    'Creating global \'for\' variable. Should be \'for (var'
-    'Missing \'()\' invoking a constructor.'    ##  covered by coffeelint rule non_empty_constructor_needs_parens
-    'Bad number \'2e308\'.'
+
+errorsToSkip                = {}
+errorsToSkip[ error.code ]  = error for error in [
+
+    ##  > Confusing use of '{a}'.
+    ##
+    code:           'W018'
+    args:           a: '!'
+
+,
+    ##  > Bad number '{a}'.
+    ##
+    code:           'W045'
+    args:           a: '2e308'
+
+,
+    ##  > Missing '()' invoking a constructor.
+    ##
+    ##  Note: covered by coffeelint rule non_empty_constructor_needs_parens
+    ##
+    code:           'W058'
+
+,
+    ##  > Creating global 'for' variable. Should be 'for (var {a} ...'.
+    ##
+    code:           'W088'
+    args:            undefined     ##  because it don't matter what `{a}` is.
+
+,
+    ##  > Wrap the /regexp/ literal in parens to disambiguate the slash operator.
+    ##
+    ##  Note: removed from jshint as of version `2.4.1`.
+    ##
+    code:           'W092'
+
+,
+    ##  > Did you mean to return a conditional instead of an assignment?
+    ##
+    code:           'W093'
+
+,
 ]

@@ -90,6 +142,15 @@
             ##
             _.compact( jshint.errors )

+                ##  Get rid of errors that don't apply to coffee very well
+                ##
+                .filter( ( error ) ->
+
+                    skip = errorsToSkip[ error.code ]
+
+                    return not( skip ) or ( skip.args and ( supplant( error.raw, skip.args ) isnt error.reason ))
+                )
+
                 ##  Convert errors to use coffee source locations instead of js locations
                 ##
                 .map( ( error ) ->
@@ -104,10 +165,6 @@
                         character:  if col?  then col  + 1 else '?'
                     )
                 )
-
-                ##  Get rid of errors that don't apply to coffee very well
-                ##
-                .filter( ( error ) -> not _.any( errorsToSkip, ( to_skip ) -> error.reason.indexOf( to_skip ) >= 0 ) )
     )


Fix default path option

Fix default path option

--- lib/sshfs-node.coffee
+++ lib/sshfs-node.coffee
@@ -48,7 +48,7 @@
   if options && options.port
     portOption = util.format '-o port=%d', options.port

-  pathOption = ''
+  pathOption = '/'
   if options && options.path
     pathOption = util.format '%s', options.path


register and reuse markers

register and reuse markers

--- lib/hole.coffee
+++ lib/hole.coffee
@@ -11,27 +11,41 @@
     @headIndices = @indicesOf text, /\{!/
     @tailIndices = @indicesOf text, /!\}/

+    # register all markers first
+    for headIndex, i in @headIndices
+      console.log 'registering hole marker'
+      tailIndex = @tailIndices[i]
+
+      pointHead = @agda.editor.buffer.positionForCharacterIndex headIndex
+      markerHead = @agda.editor.markBufferPosition pointHead,
+        hole: true
+        type: 'head'
+        index: i
+
+      pointTail = @agda.editor.buffer.positionForCharacterIndex tailIndex
+      markerTail = @agda.editor.markBufferPosition pointTail,
+        hole: true
+        type: 'tail'
+        index: i

     @agda.editor.cursors[0].on 'moved', @skipHandler
+
+  findHoleMarkers: -> @agda.editor.findMarkers hole: true

   skipHandler: (event) =>
       cursorOld = event.oldBufferPosition
       cursorNew = event.newBufferPosition

-      for headIndex, i in @headIndices
-        tailIndex = @tailIndices[i]
+      @findHoleMarkers().map (marker) =>

-        pointHead = @agda.editor.buffer.positionForCharacterIndex headIndex
-        markerHead = @agda.editor.markBufferPosition pointHead
+        attrs = marker.getAttributes()

-        pointTail = @agda.editor.buffer.positionForCharacterIndex tailIndex
-        markerTail = @agda.editor.markBufferPosition pointTail
-
-        # skip '{!'
-        @skipBorder cursorOld, cursorNew, markerHead, 'left'
-
-        # skip '!}'
-        @skipBorder cursorOld, cursorNew, markerTail, 'right'
+        if attrs.type is 'head'
+          # skip '{!'
+          @skipBorder cursorOld, cursorNew, marker, 'left'
+        else
+          # skip '!}'
+          @skipBorder cursorOld, cursorNew, marker, 'right'

   skipBorder: (cursorOld, cursorNew, marker, direction) ->


Detecting current project path correctly. Fixed #23.

--- lib/init.coffee
+++ lib/init.coffee
@@ -35,12 +35,16 @@
     os = require 'os'
     fs = require 'fs'
     path = require 'path'
-    projectPath = ->
-      atom.project.getPaths()[0]
-    isMixProject = ->
-      fs.existsSync(projectPath() + '/mix.exs')
+    projectPath = (textEditor) ->
+      editorPath = textEditor.getPath()
+      for projectPathCandidate in atom.project.getPaths()
+        if editorPath.indexOf(projectPathCandidate) == 0
+          return projectPathCandidate
+      null
+    isMixProject = (textEditor) ->
+      fs.existsSync(projectPath(textEditor) + '/mix.exs')
     isTestFile = (textEditor) ->
-      relativePath = path.relative(projectPath(), textEditor.getPath())
+      relativePath = path.relative(projectPath(textEditor), textEditor.getPath())
       relativePath.split(path.sep)[0] == 'test'
     isForcedElixirc = =>
       @forceElixirc
@@ -61,7 +65,7 @@
         #type: reResult[1]
         type: "Error"
         text: reResult[4]
-        filePath: projectPath() + '/' + reResult[2]
+        filePath: projectPath(textEditor) + '/' + reResult[2]
         range: helpers.rangeFromLineNumber(textEditor, reResult[3] - 1)
     parseWarning = (row, textEditor) ->
       re = ///
@@ -75,7 +79,7 @@
       ret =
         type: "Warning"
         text: reResult[3]
-        filePath: projectPath() + '/' + reResult[1]
+        filePath: projectPath(textEditor) + '/' + reResult[1]
         range: helpers.rangeFromLineNumber(textEditor, reResult[2] - 1)
     handleResult = (textEditor) ->
       (compileResult) ->
@@ -87,26 +91,26 @@
     getFilePathDir = (textEditor) ->
       filePath = textEditor.getPath()
       path.dirname(filePath)
-    getOpts = ->
+    getOpts = (textEditor) ->
       opts =
-        cwd: projectPath()
+        cwd: projectPath(textEditor)
         throwOnStdErr: false
         stream: 'both'
     getDepsPa = (textEditor) ->
       env = if isTestFile(textEditor) then "test" else "dev"
       buildDir = path.join("_build", env, "lib")
-      fs.readdirSync(path.join(projectPath(), buildDir)).map (item) ->
-        path.join(projectPath(), buildDir, item, "ebin")
+      fs.readdirSync(path.join(projectPath(textEditor), buildDir)).map (item) ->
+        path.join(projectPath(textEditor), buildDir, item, "ebin")
     lintElixirc = (textEditor) =>
       elixircArgs = [
         "--ignore-module-conflict", "--app", "mix", "--app", "ex_unit", "-o", os.tmpDir(),
       ]
       elixircArgs.push "-pa", item for item in getDepsPa(textEditor)
       elixircArgs.push textEditor.getPath()
-      helpers.exec(@elixircPath, elixircArgs, getOpts())
+      helpers.exec(@elixircPath, elixircArgs, getOpts(textEditor))
         .then(handleResult(textEditor))
     lintMix = (textEditor) =>
-      helpers.exec(@mixPath, ['compile'], getOpts())
+      helpers.exec(@mixPath, ['compile'], getOpts(textEditor))
         .then (handleResult(textEditor))

     provider =
@@ -115,7 +119,7 @@
       lintOnFly: false
       name: 'Elixir'
       lint: (textEditor) =>
-        if isForcedElixirc() or not isMixProject() or isExsFile(textEditor)
+        if isForcedElixirc() or not isMixProject(textEditor) or isExsFile(textEditor)
           lintElixirc(textEditor)
         else
           lintMix(textEditor)


allow dots in the sourcedir option

allow dots in the sourcedir option

example:
```
sourcedir = .
```

--- lib/utils/ipkg.coffee
+++ lib/utils/ipkg.coffee
@@ -3,7 +3,7 @@
 Rx = require 'rx-lite'

 optionsRegexp = /opts\s*=\s*\"([^\"]*)\"/
-sourcedirRegexp = /sourcedir\s*=\s*([a-zA-Z/0-9]+)/
+sourcedirRegexp = /sourcedir\s*=\s*([a-zA-Z/0-9.]+)/

 # Find all ipkg-files in a directory and returns
 # an observable of an array of files


添加是否异步执行的判断

添加是否异步执行的判断

--- lib/utils/util.coffee
+++ lib/utils/util.coffee
@@ -110,7 +110,10 @@
     fs.remove path, callback

   isFileExist: (path, cb) ->
-    fs.exists path, cb
+    if typeof cb is 'function'
+      fs.exists path, cb
+    else
+      fs.existsSync path

   store: (namespace, data) ->
     if data


Add atom telemetry consent

Add atom telemetry consent

--- atom/config/config.cson
+++ atom/config/config.cson
@@ -1,5 +1,6 @@
 "*":
   core:
+    telemetryConsent: "limited"
     themes: [
       "atom-material-ui"
       "atom-dark-syntax"


new tyku formula

new tyku formula

--- views/components/ship-parts/topalert.cjsx
+++ views/components/ship-parts/topalert.cjsx
@@ -27,33 +27,44 @@
     cond: cond

 # Tyku
-# 制空値 = [(艦載機の対空値) × √(搭載数)] の総計 + 熟練補正
+# 制空値= ∑ [艦載機の対空値 x √(搭載数) + √(熟練値/10) + 机种制空加值 ] ( [ ] 方括号代表取整)
+
+aircraftExpTable = [0, 10, 25, 40, 55, 70, 85, 100, 121]
+
+aircraftLevelBonus = {
+  '6': [0, 0, 2, 5, 9, 14, 14, 22, 22],
+  '7': [0, 0, 0, 0, 0, 0, 0, 0, 0],
+  '8': [0, 0, 0, 0, 0, 0, 0, 0, 0],
+  '11': [0, 1, 1, 1, 1, 3, 3, 6, 6],
+  '45': [0, 0, 0, 0, 0, 0, 0, 0, 0]
+}
+
 getTyku = (deck) ->
   {$ships, $slotitems, _ships, _slotitems} = window
-  basicTyku = alvTyku = totalTyku = 0
+  minTyku = maxTyku = 0
   for shipId in deck.api_ship
     continue if shipId == -1
     ship = _ships[shipId]
     for itemId, slotId in ship.api_slot
       continue unless itemId != -1 && _slotitems[itemId]?
       item = _slotitems[itemId]
+      tempTyku = 0.0
       # Basic tyku
+
       if item.api_type[3] in [6, 7, 8]
-        basicTyku += Math.floor(Math.sqrt(ship.api_onslot[slotId]) * item.api_tyku)
-      else if item.api_type[3] == 10 && item.api_type[2] == 11
-        basicTyku += Math.floor(Math.sqrt(ship.api_onslot[slotId]) * item.api_tyku)
-      # Alv
-      if item.api_type[3] == 6 && item.api_alv > 0 && item.api_alv <= 7
-        alvTyku += [0, 1, 4, 6, 11, 16, 17, 25][item.api_alv]
-      else if item.api_type[3] in [7, 8] && item.api_alv == 7
-        alvTyku += 3
-      else if item.api_type[3] == 10 && item.api_type[2] == 11 && item.api_alv == 7
-        alvTyku += 9
-  totalTyku = basicTyku + alvTyku
-
-  basic: basicTyku
-  alv: alvTyku
-  total: totalTyku
+        tempTyku += Math.sqrt(ship.api_onslot[slotId]) * item.api_tyku
+        tempTyku += aircraftLevelBonus[item.api_type[3]][item.api_alv]
+        minTyku = Math.floor(tempTyku + Math.sqrt(aircraftExpTable[item.api_alv] / 10))
+        maxTyku = Math.floor(tempTyku + Math.sqrt(aircraftExpTable[item.api_alv + 1] / 10))
+
+      else if item.api_type[3] == 10 && (item.api_type[2] == 11 || item.api_type[2] == 45)
+        tempTyku += Math.sqrt(ship.api_onslot[slotId]) * item.api_tyku
+        tempTyku += aircraftLevelBonus[item.api_type[2]][item.api_alv]
+        minTyku = Math.floor(tempTyku + Math.sqrt(aircraftExpTable[item.api_alv] / 10))
+        maxTyku = Math.floor(tempTyku + Math.sqrt(aircraftExpTable[item.api_alv + 1] / 10))
+
+  min: minTyku
+  max: maxTyku

 # Saku (2-5 旧式)
 # 偵察機索敵値×2 ＋ 電探索敵値 ＋ √(艦隊の装備込み索敵値合計 - 偵察機索敵値 - 電探索敵値)
@@ -296,7 +307,7 @@
       if @props.mini

           Lv. {@messages.totalLv}
-          {__ 'Fighter Power'}: {@messages.tyku.total}
+          {__ 'Fighter Power'}: {@messages.tyku.min}
           {__ 'LOS'}: {@messages.saku25a.total}

       else
@@ -306,10 +317,10 @@

               <OverlayTrigger placement='bottom' overlay={
                 <Tooltip id='topalert-FP'>
-                  {__ 'Basic FP'}: {@messages.tyku.basic} {__ 'Rank bonuses'}: {@messages.tyku.alv}
+                  {__ 'Basic FP'}: {@messages.tyku.min} {__ 'Rank bonuses'}: {@messages.tyku.max}
                 </Tooltip>
               }>
-                {__ 'Fighter Power'}: {@messages.tyku.total}
+                {__ 'Fighter Power'}: {@messages.tyku.min}
               </OverlayTrigger>


Use Workspace::observeTextEditors

Use Workspace::observeTextEditors

--- lib/whitespace.coffee
+++ lib/whitespace.coffee
@@ -4,7 +4,7 @@
 class Whitespace
   constructor: ->
     @subscriptions = new CompositeDisposable
-    @subscriptions.add atom.workspace.eachEditor (editor) =>
+    @subscriptions.add atom.workspace.observeTextEditors (editor) =>
       @handleEvents(editor)

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


make normal mode separate function with same signature as insert mode

--- assets/js/keyBindings.coffee
+++ assets/js/keyBindings.coffee
@@ -717,19 +717,21 @@
       view.addCharsAtCursor [key], {cursor: 'pastEnd'}
       do @menu.update

-  processOnce: (keyStream) ->
+  processNormalMode: (keyStream, bindings = @bindings[MODES.NORMAL], repeat = 1) ->

     # useful when you expect a motion
-    getMotion = (motionKey, bindings = @bindings[MODES.NORMAL]) =>
-      [repeat, motionKey] = getRepeat motionKey
+    getMotion = (motionKey, bindings, repeat = 1) =>
+      [motionRepeat, motionKey] = getRepeat motionKey
+      repeat = repeat * motionRepeat
+
       if motionKey == null
         do keyStream.wait
-        return null
+        return [null, repeat]

       info = bindings[motionKey] || {}
       if not info.motion
         do keyStream.forget
-        return null
+        return [null, repeat]

       fn = null

@@ -737,18 +739,16 @@
         key = do keyStream.dequeue
         if key == null
           do keyStream.wait
-          return null
+          return [null, repeat]
         fn = info.continue.bind @, key

       else if info.bindings
-        answer = (getMotion null, info.bindings)
+        answer = (getMotion null, info.bindings, repeat)
         return answer
       else if info.fn
         fn = info.fn

-      # TODO: this is weird... just return another argument
-      fn.repeat = repeat
-      return fn
+      return [fn, repeat]

     # takes key, returns repeat number and key
     getRepeat = (key = null) =>
@@ -767,125 +767,119 @@
         if key == null then return [null, null]
       return [parseInt(numStr), key]

-    processNormalMode = (bindings, repeat=1) =>
-      [newrepeat, key] = do getRepeat
+    [newrepeat, key] = do getRepeat
+    if key == null then return do keyStream.wait
+    # TODO: something better for passing repeat through?
+    repeat = repeat * newrepeat
+
+    fn = null
+    args = []
+
+    if not (key of bindings)
+      if 'MOTION' of bindings
+        info = bindings['MOTION']
+
+        # note: this uses original bindings to determine what's a motion
+        [motion, repeat] = getMotion key, @bindings[MODES.NORMAL], repeat
+        if motion == null then return do keyStream.forget
+
+        cursor = do @view.cursor.clone
+        for i in [1..repeat]
+          motion cursor, 'pastEnd'
+
+        args.push cursor
+      else
+        return do keyStream.forget
+    else
+      info = bindings[key] || {}
+
+    if info.bindings
+      return @processNormalMode keyStream, info.bindings, repeat
+
+    if info.motion
+      # note: this uses *new* bindings to determine what's a motion
+      [motion, repeat] = getMotion key, bindings, repeat
+      if motion == null then return
+
+      for j in [1..repeat]
+        motion @view.cursor, ''
+      return do keyStream.forget
+
+    if info.menu
+      @setMode MODES.MENU
+      @menu = new Menu @menuDiv, (info.menu.bind @, @view)
+      do @menu.render
+      return do keyStream.forget
+
+    if info.continue
+      key = do keyStream.dequeue
       if key == null then return do keyStream.wait
-      # TODO: something better for passing repeat through?
-      repeat = repeat * newrepeat
-
-      fn = null
-      args = []
-
-      if not (key of bindings)
-        if 'MOTION' of bindings
-          info = bindings['MOTION']
-
-          # note: this uses original bindings to determine what's a motion
-          motion = getMotion key
-          if motion == null then return do keyStream.forget
-
-          cursor = do @view.cursor.clone
-          for i in [1..repeat]
-            for j in [1..motion.repeat]
-              motion cursor, 'pastEnd'
-
-          args.push cursor
-        else
-          return do keyStream.forget
-      else
-        info = bindings[key] || {}
-
-      if info.bindings
-        return processNormalMode info.bindings, repeat
-
-      if info.motion
-        # note: this uses *new* bindings to determine what's a motion
-        motion = getMotion key, bindings
-        if motion == null then return
-
-        for j in [1..repeat]
-          motion @view.cursor, ''
-        return do keyStream.forget
-
-      if info.menu
-        @setMode MODES.MENU
-        @menu = new Menu @menuDiv, (info.menu.bind @, @view)
-        do @menu.render
-        return do keyStream.forget
-
-      if info.continue
-        key = do keyStream.dequeue
-        if key == null then return do keyStream.wait
-
-        fn = info.continue
-        args.push key
-      else if info.fn
-        fn = info.fn
-
-      if fn
-        context = {
-          view: @view,
-          repeat: repeat,
-          keybindingsDiv: @keybindingsDiv,
-          setMode: @setMode
-        }
-        fn.apply context, args
-
-        if info.insert
-          @setMode MODES.INSERT
-          return
-        if info.drop
-          return do keyStream.forget
-        else
-          return do keyStream.save
-
-      if info.name == 'RECORD_MACRO'
-        if @recording == null
-          nkey = do keyStream.dequeue
-          if nkey == null then return do keyStream.wait
-          @recording = new KeyStream
-          @recording_key = nkey
-        else
-          macro = @recording.queue
-          do macro.pop # pop off the RECORD_MACRO itself
-          @macros[@recording_key] = macro
-          @recording = null
-          @recording_key = null
-        return do keyStream.forget
-      if info.name == 'PLAY_MACRO'
-          nkey = do keyStream.dequeue
-          if nkey == null then return do keyStream.wait
-          recording = @macros[nkey]
-          if recording == undefined then return do keyStream.forget
-
-          for i in [1..repeat]
-            # the recording shouldn't save, (i.e. no @view.save)
-            recordKeyStream = new KeyStream recording
-            @processKeys recordKeyStream
-          # but we should save the macro-playing sequence itself
-          return do keyStream.save
-
-      if info.name == 'REPLAY'
-        for i in [1..repeat]
-          newStream = new KeyStream @keyStream.lastSequence
-          newStream.on 'save', () =>
-            do @view.save
-          @processKeys newStream
+
+      fn = info.continue
+      args.push key
+    else if info.fn
+      fn = info.fn
+
+    if fn
+      context = {
+        view: @view,
+        repeat: repeat,
+        keybindingsDiv: @keybindingsDiv,
+        setMode: @setMode
+      }
+      fn.apply context, args
+
+      if info.insert
+        @setMode MODES.INSERT
+        return
+      if info.drop
         return do keyStream.forget
       else
-        return do keyStream.forget
-
-    # if key not in @reverseBindings then return
-
-    # name = @reverseBindings[key]
-    # handler = @handlers[name]
-    # do handler
-
+        return do keyStream.save
+
+    if info.name == 'RECORD_MACRO'
+      if @recording == null
+        nkey = do keyStream.dequeue
+        if nkey == null then return do keyStream.wait
+        @recording = new KeyStream
+        @recording_key = nkey
+      else
+        macro = @recording.queue
+        do macro.pop # pop off the RECORD_MACRO itself
+        @macros[@recording_key] = macro
+        @recording = null
+        @recording_key = null
+      return do keyStream.forget
+    if info.name == 'PLAY_MACRO'
+        nkey = do keyStream.dequeue
+        if nkey == null then return do keyStream.wait
+        recording = @macros[nkey]
+        if recording == undefined then return do keyStream.forget
+
+        for i in [1..repeat]
+          # the recording shouldn't save, (i.e. no @view.save)
+          recordKeyStream = new KeyStream recording
+          @processKeys recordKeyStream
+        # but we should save the macro-playing sequence itself
+        return do keyStream.save
+
+    if info.name == 'REPLAY'
+      for i in [1..repeat]
+        newStream = new KeyStream @keyStream.lastSequence
+        newStream.on 'save', () =>
+          do @view.save
+        @processKeys newStream
+      return do keyStream.forget
+    else
+      return do keyStream.forget
+
+
+  processOnce: (keyStream) ->
     if @mode == MODES.INSERT
       return @processInsertMode keyStream

     if @mode == MODES.NORMAL
-      return processNormalMode @bindings[MODES.NORMAL]
+      return @processNormalMode keyStream

     if @mode = MODES.MENU
       key = do keyStream.dequeue


! fix map data reference

! fix map data reference

--- lib/main.coffee
+++ lib/main.coffee
@@ -7,7 +7,7 @@

   run: () =>
     @assets.load(@assetsLoaded);
-    @map.data.loadInitialMapData(@dataLoaded);
+    @stage.map.data.loadInitialMapData(@dataLoaded);

   assetsLoaded: () =>
     if @dataLoaded == true


Do a naive component update whenever the update manager emits a change event

--- lib/main.coffee
+++ lib/main.coffee
@@ -58,15 +58,17 @@
     unless @aboutView?
       AboutView ?= require './about-view.js'
       updateManager = getUpdateManager()
-      debugger
       @aboutView = new AboutView({
         uri: AboutURI,
         updateManager,
         availableVersion: updateManager.getAvailableVersion()
       })

-      updateManager.onDidChange => @aboutView.update({updateManager})
-    debugger
+      updateManager.onDidChange => @aboutView.update({
+        uri: AboutURI,
+        updateManager,
+        availableVersion: updateManager.getAvailableVersion()
+      })
     @aboutView

   isUpdateAvailable: ->


Further clarify settings wording

Further clarify settings wording

--- lib/main.coffee
+++ lib/main.coffee
@@ -88,22 +88,22 @@
       default: true
       order: 5
     showErrorTabLine:
-      title: 'Show Line Errors Tab in the Status Bar'
+      title: 'Show "Line" Tab in the Status Bar'
       type: 'boolean'
       default: false
       order: 5
     showErrorTabFile:
-      title: 'Show File Errors Tab in the Status Bar'
+      title: 'Show "File" Tab in the Status Bar'
       type: 'boolean'
       default: true
       order: 5
     showErrorTabProject:
-      title: 'Show Project Errors Tab in the Status Bar'
+      title: 'Show "Project" Tab in the Status Bar'
       type: 'boolean'
       default: true
       order: 5
     statusIconScope:
-      title: 'Scope of Error Messages to Show in Status Icon'
+      title: 'Scope of Linter Messages to Show in Status Icon'
       type: 'string'
       enum: ['File', 'Line', 'Project']
       default: 'Project'