resolve immediately for old kites

resolve immediately for old kites

--- client/Main/VirtualizationController.coffee
+++ client/Main/VirtualizationController.coffee
@@ -306,9 +306,8 @@
       # terminal kite.
       kite.ready =>
         @terminalKites[alias] = @getKite vm, 'terminal'
-        resolve()
+      resolve()
     .nodeify callback
-

   listenToVmState: (vm, kite) ->
     alias = vm.hostnameAlias


Ace: Deprecate resizeEditor method.

Ace: Deprecate resizeEditor method.

--- client/ace/lib/acefindandreplaceview.coffee
+++ client/ace/lib/acefindandreplaceview.coffee
@@ -40,7 +40,6 @@
   close: (fireEvent = yes) ->

     @hide()
-    @resizeEditor 0
     @findInput.setValue    ''
     @replaceInput.setValue ''
     @emit 'FindAndReplaceViewClosed'  if fireEvent
@@ -59,21 +58,7 @@
       height = 26 # we need to shrink editor height to fit all content

     @[method] cssName
-    @resizeEditor height
-
-
-  resizeEditor: (height) ->
-
-    if @getDelegate()?.ace?
-      {ace} = @getDelegate()
-      return unless ace.editor
-
-      ace.setHeight ace.getHeight() + @lastHeightTakenFromAce - height
-      ace.editor.resize yes
-      @lastHeightTakenFromAce = height
-
-
-  lastHeightTakenFromAce: 0
+    @emit 'FindAndReplaceViewShown', withReplace

   setTextIntoFindInput: (text) ->


Teambuilder: Fix various bugs, autofocus new pokes.

--- client/app/js/views/teambuilder_view.coffee
+++ client/app/js/views/teambuilder_view.coffee
@@ -4,7 +4,7 @@

   events:
     'click .pokemon_list li': 'clickPokemon'
-    'click .add_pokemon': 'addEmptyPokemon'
+    'click .add_pokemon': 'addNewPokemon'
     'click .save_team': 'saveTeam'
     'change .species_list': 'changeSpecies'
     'change .selected_nature': 'changeNature'
@@ -38,13 +38,12 @@
     @listenTo(@collection, 'add remove', @renderPokemonList)

     @loadTeam()
-    @selected = 0

     @render()

   clickPokemon: (ev) =>
     $listItem = $(ev.target)
-    index = $('.pokemon_list li').index($listItem)
+    index = @$('.pokemon_list li').index($listItem)
     @setSelectedIndex(index)

   loadTeam: =>
@@ -53,10 +52,14 @@
       teamJSON = JSON.parse(teamJSON)
       @collection.reset(teamJSON)
     else
-      @addEmptyPokemon()
+      @addNewPokemon()

   addEmptyPokemon: =>
     @collection.add(new Pokemon())
+
+  addNewPokemon: =>
+    @addEmptyPokemon()
+    @$('.pokemon_list li').last().click()

   saveTeam: =>
     teamJSON = @collection.toJSON()
@@ -107,7 +110,7 @@
     pokemon.set('hiddenPowerType', type.toLowerCase())

   keyupMoves: (e) =>
-    $table = @$('.table-moves')
+    $table = @getActivePokemonView().find('.table-moves')
     $allMoves = $table.find('tbody tr')
     switch e.which
       when 9, 13  # [Tab]/[Enter]; we're selecting the active move.
@@ -132,7 +135,7 @@
         @filterMovesBy(moveName)

   filterMovesBy: (moveName) =>
-    $table = @$('.table-moves')
+    $table = @getActivePokemonView().find('.table-moves')
     $allMoves = $table.find('tbody tr')
     moveRegex = new RegExp(moveName, "i")
     $moves = $allMoves.filter ->
@@ -147,14 +150,14 @@
     $table.removeClass('hidden')

   hideMoves: =>
-    $table = @$('.table-moves')
+    $table = @getActivePokemonView().find('.table-moves')
     $table.addClass('hidden')

   selectMove: (e) =>
     $this = $(e.currentTarget)
     moveName = $this.data('move-id')
     type = MoveData[moveName].type.toLowerCase()
-    $moves = @$('.selected_moves')
+    $moves = @getActivePokemonView().find('.selected_moves')
     $input = $moves.find('input').first()
     $input.replaceWith("""
#{moveName}
""")
     $moves.find('input').first().focus()


Fixed: app installed from Git weren't working if they weren't in the marketplace. Close #252

--- client/app/views/popover_description.coffee
+++ client/app/views/popover_description.coffee
@@ -23,7 +23,11 @@
         # retrieves from market if app is official or not
         appsCollection = new ApplicationCollection().fetchFromMarket()
         app = appsCollection.get @model.get('slug')
-        @model.set 'comment', app.get('comment')
+
+        # By default, apps are 'community contribution'.
+        # Used for "install from Git"
+        comment = if app? then app.get('comment') else 'community contribution'
+        @model.set 'comment', comment
         return super()

     afterRender: ->


print out the deck if there's an unknown card, to aid debugging

--- server/deckdata.coffee
+++ server/deckdata.coffee
@@ -27,6 +27,7 @@
       features.unique[card] = 1
       features.nUnique += 1
       if not card_info[card]?
+        console.log(deck)
         console.log("no such card: #{card}")
       if card_info[card].isAction
         features.nActions += count


Adapted ApproveFindMistake to new components

--- client/components/ApproveFindMistake.coffee
+++ client/components/ApproveFindMistake.coffee
@@ -3,12 +3,15 @@

 import ConnectedComponent from '../lib/ConnectedComponent'

-import {ReviewResult} from './ReviewResult'
+import {SubmitListWithDiff} from './ReviewResult'
+import Submit from './Submit'
+
+SubmitComponent = (props) ->
+    <Submit submit={props.currentSubmit} showHeader/>

 ApproveFindMistake = (props) ->
-    fakeResult =
-        _id: ""
-    <ReviewResult submits={props.data.submits} user={{}} bestSubmits={[]} me={{}} result={fakeResult}/>
+    console.log props.data.submits
+    <SubmitListWithDiff submits={props.data.submits} SubmitComponent={SubmitComponent}/>

 options = {
     urls: (props) ->


ideview: tell appcontroller to pen file instead of explicitly creating it

it handles events and lots of other things, it's better for it to follow same code path.

--- client/ide/lib/views/tabview/ideview.coffee
+++ client/ide/lib/views/tabview/ideview.coffee
@@ -458,7 +458,7 @@
     items =
       'New File'          : callback : =>
          newFile = FSHelper.createFileInstance { path: @getDummyFilePath(), machine }
-         @createEditor(newFile)
+         kd.singletons.appManager.tell 'IDE', 'openFile', newFile
       'New Terminal'      : children : terminalSessions
       # 'New Browser'       : callback : => @createPreview()
       'New Drawing Board' :


Update name-expander.coffee

Update name-expander.coffee

--- scripts/name-expander.coffee
+++ scripts/name-expander.coffee
@@ -18,7 +18,7 @@
 marksoho = "Mark Soho"
 nmathrani = "Neeraj Mathrani"

-noc_members = ["Carter1", "Cames1", "jdowns", "jdowns1", "jtramullas", "nstuhring", "ctrytten1", "ctrytten", "akim", "nsanches", "jkim", "ebednick", "nhooper"]
+noc_members = ["Carter1", "Cames1", "jdowns", "jdowns1", "jtramullas", "nstuhring", "ctrytten1", "ctrytten", "akim", "nsanches", "jkim", "ebednik", "nhooper"]

 dert_mermbers = ["CDERCKERSERN", "MAHCHERLERVER", "JERS HERERTH", "JKERL", "JERMAH WERNSER", "CHIN YERN", "KERLERN", "ERNDRERW GERSERN", "JERSTEN CERMPBELL"]


Tests: IDE tests are now wait for VM running state.

YES!

--- tests/src/ide/ide.coffee
+++ tests/src/ide/ide.coffee
@@ -10,6 +10,7 @@
   openFile: (browser) ->

     user = helpers.beginTest(browser)
+    helpers.waitForVMRunning(browser)

     helpers.openFolderContextMenu(browser, user, 'Web')

@@ -33,13 +34,17 @@
   createNewFile: (browser) ->

     user = helpers.beginTest(browser)
+    helpers.waitForVMRunning(browser)
     helpers.createFile(browser, user)
     browser.end()

   deleteFile: (browser) ->

-    user         = helpers.beginTest(browser)
+    user = helpers.beginTest(browser)
+
+    helpers.waitForVMRunning(browser)
+
     filename     = helpers.createFile(browser, user)
     webPath      = '/home/' + user.username + '/Web'
     fileSelector = "span[title='" + webPath + '/' + filename + "']"
@@ -50,7 +55,10 @@

   rename: (browser) ->

-    user         = helpers.beginTest(browser)
+    user = helpers.beginTest(browser)
+
+    helpers.waitForVMRunning(browser)
+
     filename     = helpers.createFile(browser, user)
     webPath      = '/home/' + user.username + '/Web'
     fileSelector = "span[title='" + webPath + '/' + filename + "']"
@@ -72,12 +80,12 @@
       .end()

-
-    user = helpers.beginTest(browser)
-
   duplicate: (browser) ->

-    user          = helpers.beginTest(browser)
+    user = helpers.beginTest(browser)
+
+    helpers.waitForVMRunning(browser)
+
     filename      = helpers.createFile(browser, user)
     newFileName   = filename.split('.txt').join('_1.txt')
     webPath       = '/home/' + user.username + '/Web'
@@ -101,7 +109,10 @@

   collapse: (browser) ->

-    user        = helpers.beginTest(browser)
+    user = helpers.beginTest(browser)
+
+    helpers.waitForVMRunning(browser)
+
     webPath     = '/home/' + user.username + '/Web'
     webSelector = "span[title='" + webPath + "']"
     file        = "span[title='" + webPath + '/' + 'index.html' + "']"
@@ -122,7 +133,10 @@

   makeTopFolder: (browser) ->

-    user           = helpers.beginTest(browser)
+    user = helpers.beginTest(browser)
+
+    helpers.waitForVMRunning(browser)
+
     webPath        = '/home/' + user.username + '/Web'
     filename       = helpers.createFile(browser, user)
     webSelector    = "span[title='" + webPath + "']"
@@ -151,11 +165,16 @@

   createWorkspaceFromVmList: (browser) ->

+    helpers.beginTest(browser)
+    helpers.waitForVMRunning(browser)
     helpers.createWorkspace(browser)
     browser.end()

   deleteWorkspace: (browser) ->
+
+    helpers.beginTest(browser)
+    helpers.waitForVMRunning(browser)

     workspaceName = helpers.createWorkspace(browser)

@@ -178,6 +197,7 @@
   runCommandOnTerminal: (browser) ->

     helpers.beginTest(browser)
+    helpers.waitForVMRunning(browser)

     time = Date.now()

@@ -193,6 +213,7 @@
   openEditorSettings: (browser) ->

     helpers.beginTest(browser)
+    helpers.waitForVMRunning(browser)

     browser
       .waitForElementVisible    '.kdtabhandle-tabs .settings', 20000
@@ -203,7 +224,10 @@

   createNewFileFromMachineHeader: (browser) ->

-    user          = helpers.beginTest(browser)
+    user = helpers.beginTest(browser)
+
+    helpers.waitForVMRunning(browser)
+
     filePath      = '/home/' + user.username
     paragraph     = helpers.getFakeText()
     fileName      = paragraph.split(' ')[0] + '.txt'
@@ -225,6 +249,7 @@
   splitPanesVertically: (browser) ->

     helpers.beginTest(browser)
+    helpers.waitForVMRunning(browser)

     browser
       .waitForElementVisible '.panel-1', 20000
@@ -245,6 +270,7 @@
   splitPanesHorizontally: (browser) ->

     helpers.beginTest(browser)
+    helpers.waitForVMRunning(browser)

     browser
       .waitForElementVisible '.panel-1', 20000
@@ -265,12 +291,17 @@

   splitPanesUndo: (browser) ->

+    helpers.beginTest(browser)
+    helpers.waitForVMRunning(browser)
+
     helpers.splitPanesUndo(browser)
     browser.end()

   undoSplitPanesNotShowOnList: (browser) ->

+    helpers.beginTest(browser)
+    helpers.waitForVMRunning(browser)
     helpers.splitPanesUndo(browser)

     browser
@@ -283,13 +314,12 @@
   enterFullScreen: (browser) ->

     helpers.beginTest(browser)
+    helpers.waitForVMRunning(browser)

     browser
       .waitForElementVisible     '.application-tab-handle-holder', 20000
       .click                     '.application-tab-handle-holder .plus'
       .waitForElementVisible     '.context-list-wrapper', 20000
       .click                     '.context-list-wrapper li.enter-fullscreen'
-      .waitForElementNotPresent  '.terminal-pane span.inverse', 20000 # Assertion
-      .end()
-
-
+      .waitForElementVisible     '.ws-tabview.fullscren', 20000 # Assertion
+      .end()


display location of weather in response

display location of weather in response

--- scripts/weather.coffee
+++ scripts/weather.coffee
@@ -14,6 +14,7 @@
 # Author:
 #   markstory
 #   mbmccormick
+#   thehippo
 env = process.env

 forecastIoUrl = 'https://api.forecast.io/forecast/' + process.env.HUBOT_FORECAST_API_KEY + '/'
@@ -24,13 +25,14 @@
     .get() (err, res, body) ->
       try
         body = JSON.parse body
+        address = body.results[0].formatted_address
         coords = body.results[0].geometry.location
       catch err
         err = "Could not find #{location}"
-        return cb(msg, null, err)
-      cb(msg, coords, err)
+        return cb(msg, null, null, err)
+      cb(msg, coords, address, err)

-lookupWeather = (msg, coords, err) ->
+lookupWeather = (msg, coords, address, err) ->
   return msg.send err if err
   return msg.send "You need to set env.HUBOT_FORECAST_API_KEY to get weather data" if not env.HUBOT_FORECAST_API_KEY

@@ -45,10 +47,10 @@
       return msg.send "Could not parse weather data."
     humidity = (current.humidity * 100).toFixed 0
     temperature = getTemp(current.temperature)
-    text = "It is currently #{temperature} #{current.summary}, #{humidity}% humidity"
+    text = "It is currently #{temperature} #{current.summary}, #{humidity}% humidity in #{address}"
     msg.send text

-lookupForecast = (msg, coords, err) ->
+lookupForecast = (msg, coords, address, err) ->
   return msg.send err if err
   return msg.send "You need to set env.HUBOT_FORECAST_API_KEY to get weather data" if not env.HUBOT_FORECAST_API_KEY

@@ -63,7 +65,7 @@
       dayAfter = forecast[2]
     catch err
       return msg.send 'Unable to parse forecast data.'
-    text = "The weather for:\n"
+    text = "The weather for #{address}:\n"

     appendText = (text, data) ->
       dateToday = new Date(data.time * 1000)


bugfix account deletion was not deleting messages

--- server/models/message.coffee
+++ server/models/message.coffee
@@ -128,8 +128,10 @@
 # cb - {Function}(err) for task completion
 #
 # Returns {void}
-Message.destroyByID = (messageID, cb) ->
-    Message.adapter.destroy null, messageID, cb
+Message.destroyByID = (messageID) ->
+    # @TODO, this could be a single request
+    Message.findPromised messageID
+    .then (msg) -> msg.destroyPromised()

 # Public: destroy all messages for an account
@@ -146,7 +148,7 @@
 Message.safeDestroyByAccountID = (accountID, retries = 2) ->

     destroyOne = (row) ->
-        Message.destroyByIDPromised(row.id)
+        Message.destroyByID row.id
         .delay 100 # let the DS breath

     # get LIMIT_DESTROY messages IDs in RAM


Update Safari HTML paste handler for range changes

--- src/trix/controllers/input_controller.coffee
+++ src/trix/controllers/input_controller.coffee
@@ -364,7 +364,7 @@
     types["Files"] or types["application/x-trix-document"] or types["text/html"] or types["text/plain"]

   getPastedHTMLUsingHiddenElement: (callback) ->
-    locationRange = @responder?.getLocationRange()
+    selectedRange = @responder?.getSelectedRange()

     element = makeElement(tagName: "div", editable: true, style: { position: "absolute", left: "-9999px" })
     document.body.appendChild(element)
@@ -373,7 +373,7 @@
     requestAnimationFrame =>
       html = element.innerHTML
       document.body.removeChild(element)
-      @responder?.setSelectionForLocationRange(locationRange)
+      @responder?.immediatelySetSelectedRange(selectedRange)
       callback(html)

   @proxyMethod "responder?.expandSelectionInDirection"


No need to reimplement autofocus anymore

--- src/trix/elements/trix_editor_element.coffee
+++ src/trix/elements/trix_editor_element.coffee
@@ -8,11 +8,6 @@
   id = 0

   # Contenteditable support helpers
-
-  autofocus = (element) ->
-    unless document.querySelector(":focus")
-      if element.hasAttribute("autofocus") and document.querySelector("[autofocus]") is element
-        element.focus()

   makeEditable = (element) ->
     return if element.hasAttribute("contenteditable")
@@ -146,7 +141,6 @@
     makeEditable(this)

   attachedCallback: ->
-    autofocus(this)
     @editorController ?= new Trix.EditorController(editorElement: this, html: @defaultValue = @value)
     @editorController.registerSelectionManager()
     @registerResetListener()


Add link-to helper with locale support

Add link-to helper with locale support

--- app/assets/javascripts/cmtool/html_edit.js.coffee
+++ app/assets/javascripts/cmtool/html_edit.js.coffee
@@ -43,3 +43,17 @@
       text_field.hide()
       template_field.hide()
 @html_edit = new HtmlEdit()
+Handlebars.registerHelper "link-to", (url, args..., options = {})->
+  unless not url or url.match(/^http|^\/\w{2}\//)
+    # Starts with http or /:locale/ explicitly
+    if options.hash and options.hash.hasOwnProperty 'locale'
+      if locale = options.hash.locale
+        url = "/#{locale}/#{url}".replace('//', '/')
+    else if locale = $('#page_locale').val()
+      url = "/#{locale}/#{url}".replace('//', '/')
+  if options.fn
+    text = options.fn(this)
+  else
+    text = args[0]
+  link = "#{text}"
+  new Handlebars.SafeString(link)


Update hooks.coffee

Update hooks.coffee

--- app/tests/cucumber/features/_support/hooks.coffee
+++ app/tests/cucumber/features/_support/hooks.coffee
@@ -4,7 +4,7 @@
 	module.exports = ->
 		@Before ->
 			#global.expect = require('@xolvio/jasmine-expect').expect
-			server.call 'fixtures/reset'
+			Meteor.call 'fixtures/reset'
 			#server.call 'emailStub/reset'
 			#server.call 'emailStub/stub'
 			#server.call 'fixtures/stubCloudFrontClient'
@@ -19,4 +19,4 @@
 						current_period_end: 1436716844
 					} ]
 				# this call will stub stripe both on the client and server
-				Meteor.call 'stripeStub/stub', customer, done###+				Meteor.call 'stripeStub/stub', customer, done###


use a default flake8 in atom linter

use a default flake8 in atom linter

I don't remember why I had to explicitly set another one
we'll see

--- .atom/config.cson
+++ .atom/config.cson
@@ -46,8 +46,7 @@
   linter:
     lintOnFly: false
     showErrorPanel: false
-  "linter-flake8":
-    executablePath: "flake8;/home/nathdwek/.local/bin/flake8"
+  "linter-flake8": {}
   "linter-pep257": {}
   "linter-pep8": {}
   "linter-python-pep257": {}


make year slider a controlled component, and maintain state in parent component scope

--- app/assets/javascripts/components/group-page.cjsx
+++ app/assets/javascripts/components/group-page.cjsx
@@ -9,11 +9,18 @@
 GroupPage = React.createClass
   displayName: "GroupPage"

+
   getInitialState: ->
     all_institutions: []
     all_playwrights: []
+
+    # min, max year will indicate the range of years from which the user can select
+    # while start and end years will indicate the actual years the user has selected
     min_year: 1920
     max_year: 1940
+    start_year: 1920
+    end_year: 1940
+

   componentDidMount: ->
     # make a request to http://localhost:3000/subject_set_first_pages?group_id={{requested group id}}
@@ -53,6 +60,8 @@
         all_playwrights: all_playwrights
         min_year: min_year
         max_year: max_year
+        start_year: min_year
+        end_year: max_year

       @initializeFirstPageArray()

@@ -73,10 +82,10 @@
     @setState
       first_page_array: first_pages_view

+
   updateFirstPageArray: ->
     selected_institution = $(".collection-institution select").val()
     selected_playwright  = $(".collection-playwright select").val()
-    start_and_end_years  = @refs.ReactYearSlider.state.value

     # define the selection criteria to be used to filter first page objects
     selection_criteria_options = [
@@ -106,10 +115,11 @@
       keep_page = 1

       # ensure year falls within user-selected range
-      page_year = parseInt(first_page.meta_data.year)
-      if page_year < start_and_end_years[0]
+      page_year = first_page.meta_data.year
+      page_year_as_int = parseInt(page_year)
+      if page_year_as_int < @state.start_year
         keep_page = 0
-      if page_year > start_and_end_years[1]
+      if page_year_as_int > @state.end_year
         keep_page = 0

       # iterate over the selection criteria and continue filtering
@@ -125,6 +135,14 @@
       first_pages_view.push <FirstPageThumbnail page_json={page_json} key={index} />
     @setState
       first_page_array: first_pages_view
+
+
+  updateYearSliderState: (start_end_year_array) ->
+    @setState
+      start_year: start_end_year_array[0]
+      end_year: start_end_year_array[1]
+      @updateFirstPageArray
+

   render: ->
     if ! @state.group?
@@ -155,14 +173,12 @@

-
                   <ReactSlider
-                    onAfterChange={@updateFirstPageArray}
+                    onAfterChange={@updateYearSliderState}
                     ref="ReactYearSlider"
                     min={@state.min_year}
                     max={@state.max_year}
-                    defaultValue={[@state.min_year, @state.max_year]} withBars />
-
+                    value={[@state.start_year, @state.end_year]} withBars />


atom: update config

atom: update config

--- .atom/config.cson
+++ .atom/config.cson
@@ -62,6 +62,11 @@
   "vim-surround": {}
   "markdown-preview": {}
   emmet: {}
+  "find-and-replace":
+    openProjectFindResultsInRightPane: true
+  "color-picker":
+    preferredFormat: "HEX"
+    uppercaseColorValues: true
 ".source.ts.tsx":
   editor:
     preferredLineLength: 100


render hyperlink to talk subject

render hyperlink to talk subject

--- app/assets/javascripts/components/mark/index.cjsx
+++ app/assets/javascripts/components/mark/index.cjsx
@@ -77,7 +77,7 @@
     firstTask = activeWorkflow.first_task
     onFirstAnnotation = @state.taskKey == firstTask
     currentSubtool = if @state.currentSubtool then @state.currentSubtool else @getTasks()[firstTask]?.tool_config.tools?[0]
-
+    hasZooniverseId = @getCurrentSubject().meta_data.zooniverse_id?
     # direct link to this page
     pageURL = "#{location.origin}/#/mark?subject_set_id=#{@getCurrentSubjectSet().id}&selected_subject_id=#{@getCurrentSubject().id}"

@@ -186,10 +186,13 @@

             {
-              console.log 'CURRENT SUBJECT: ', @getCurrentSubject()
+              if hasZooniverseId
+                Click here to discuss this page
+              else
+                <ForumSubjectWidget subject_set={@getCurrentSubjectSet()} project={@props.project} />
+
             }
-            <ForumSubjectWidget subject_set={@getCurrentSubjectSet()} project={@props.project} />
-

+


Refactor key mapping

Refactor key mapping

--- .atom/keymap.cson
+++ .atom/keymap.cson
@@ -22,7 +22,7 @@
 # * https://atom.io/docs/latest/behind-atom-keymaps-in-depth
 #
 # If you're having trouble with your keybindings not working, try the
-# Keybinding Resolver: `Cmd+.` on OS X and `Ctrl+.` on other platforms. See the
+# Keybinding Resolver: `Cmd+.` on OS X and `Ctrl+.` on other platforms. See the,
 # Debugging Guide for more information:
 # * https://atom.io/docs/latest/hacking-atom-debugging#check-the-keybindings
 #
@@ -32,14 +32,17 @@
 # https://atom.io/docs/latest/using-atom-basic-customization#cson

 'atom-workspace atom-text-editor:not([mini])':
-  # cursor-history
-  'ctrl-[': 'last-cursor-position:previous'
-  'ctrl-]': 'last-cursor-position:next'
   # end cursor-history
   'cmd-up': 'editor:move-line-up'
   'cmd-down': 'editor:move-line-down'

-# expand-region
-'atom-text-editor':
+  # expand region
   'alt-up': 'expand-region:expand'
   'alt-down': 'expand-region:shrink'
+
+  # cursor-history
+  'ctrl-[': 'last-cursor-position:previous'
+  'ctrl-]': 'last-cursor-position:next'
+
+  # turbo javascript
+  'ctrl-enter': 'turbo-javascript:end-new-line'


use an en dash for time ranges

use an en dash for time ranges

--- app/assets/transit/lib/models/itinerary.js.coffee
+++ app/assets/transit/lib/models/itinerary.js.coffee
@@ -17,7 +17,7 @@
 		start = Transit.format_time(@get('startTime'))
 		end   = Transit.format_time(@get('endTime'))
 		total = Transit.format_duration(@get('duration') / 1000)
-		"#{start} - #{end} (#{total})"
+		"#{start}–#{end} (#{total})"

 	duration: =>
 		walk = Transit.format_duration(@get('walkTime'), true)


Use better variable name.

Use better variable name.

--- src/coffee/producttypegenerator.coffee
+++ src/coffee/producttypegenerator.coffee
@@ -97,7 +97,7 @@

     # this will iterate over the array and execute the function if the condition is passed, returning a "filtered" array
     # see http://coffeescript.org/#loops
-    (languages(h) for h in headers when h.match(regexp))
+    (languages(header) for header in headers when header.match(regexp))

   ###


hub: add loging showing time taken to json-ify messages, since this may be useful

--- salvus/hub.coffee
+++ salvus/hub.coffee
@@ -1698,7 +1698,12 @@
                     g("timed out")
             setTimeout(f, 15000) # timeout after some seconds

-        @push_data_to_client(JSON_CHANNEL, to_json(mesg))
+        t = new Date()
+        json = to_json(mesg)
+        tm = new Date() - t
+        if tm > 10
+            winston.debug("client=#{@id}, mesg.id=#{mesg.id}: time to json=#{tm}ms; length=#{json.length}")
+        @push_data_to_client(JSON_CHANNEL, json)
         if not listen
             cb?()
             return
@@ -6136,7 +6141,6 @@

 exports.start_server = start_server = (cb) ->
     # the order of init below is important
-
     winston.debug("port = #{program.port}, proxy_port=#{program.proxy_port}")
     winston.info("Using keyspace #{program.keyspace}")
     hosts = program.database_nodes.split(',')
@@ -6147,7 +6151,6 @@
     blocked (ms) ->
         # record that something blocked for over 10ms
         winston.debug("BLOCKED for #{ms}ms")
-

     async.series([
         (cb) ->


Update Route skeleton

Update Route skeleton

The `function()` always gets me, I always end up changing it to

```
model() {
}
```

What do you think?

--- snippets/file-skeletons.cson
+++ snippets/file-skeletons.cson
@@ -14,9 +14,11 @@
       import Route from '@ember/routing/route';

       export default Route.extend({
-      \tmodel: function() {
+
+      \tmodel() {
       \t\t${1:// body}
-      \t},
+      \t}
+
       });
     """


get rid of some debugging output

get rid of some debugging output

--- salvus/page/course.coffee
+++ salvus/page/course.coffee
@@ -601,9 +601,9 @@
                             set   : {project_id : project_id}
                             where : where
                         i = Math.random()
-                        console.log('about to save ',i)
+                        #console.log('about to save ',i)
                         @db.save (err) =>
-                            console.log('save #{i} done -- err',err)
+                            #console.log('save #{i} done -- err',err)
                         @update_student_view(student_id:opts.student_id)
                         opts.cb?()
             )


file_use: display items a little more readably

--- salvus/page/file_use.cjsx
+++ salvus/page/file_use.cjsx
@@ -29,8 +29,8 @@
 TODO:

 - [x] (0:30)  basic structure and plan
-- [ ] (0:15?) (0:22) sorted file use by last_edited timestamp
-- [ ] (0:30?) display items a little more readably
+- [x] (0:15?) (0:22) sorted file use by last_edited timestamp
+- [x] (0:30?) (0:24) display items a little more readably
 - [ ] (1:00?) get use of file by person to actually cause update of use
 - [ ] (1:00?) make even more readable, e.g., file type icons, layout
 - [ ] (0:30?) search
@@ -47,7 +47,7 @@

 misc = require('misc')
 {React, Actions, Store, Table, rtypes, rclass, FluxComponent}  = require('flux')
-{Loading} = require('r_misc')
+{Loading, TimeAgo} = require('r_misc')
 {User} = require('users')

 class FileUseActions extends Actions
@@ -73,8 +73,8 @@
         if @_sorted_file_use_list?
             return @_sorted_file_use_list
         v = []
-        @state.file_use.map (val,_) =>
-            v.push(val.toJS())
+        @state.file_use.map (x,_) =>
+            v.push(x.toJS())
         v.sort (a,b)->misc.cmp(b.last_edited, a.last_edited)
         @_sorted_file_use_list = v
         return v
@@ -86,15 +86,55 @@
     _change: (table, keys) =>
         @flux.getActions('file_use').setTo(file_use: table.get())

+FileUse = rclass
+    displayName: 'FileUse'
+    propTypes: ->
+        id          : rtypes.string.isRequired
+        project_id  : rtypes.string
+        path        : rtypes.string
+        last_edited : rtypes.object
+        users       : rtypes.object   # might not be given
+        user_map    : rtypes.object.isRequired
+        project_map : rtypes.object.isRequired
+
+    render_users: ->
+        if @props.users?
+            n = misc.len(@props.users)
+            i = 0
+            v = []
+            for account_id, user of @props.users
+                v.push <User key={account_id} account_id={account_id} user_map={@props.user_map} last_active={user.edit} />
+                if i < n-1
+                    v.push ,
+                i += 1
+            return v
+
+    render_last_edited: ->
+        if @props.last_edited?
+            <TimeAgo key='last_edited' date={@props.last_edited} />
+
+    render: ->
+

+
{@props.path}

+
{@props.project_map.get(@props.project_id)?.get('title')}

+            {@render_last_edited() if not @props.users?}
+            {@render_users()}
+

+
 FileUseViewer = rclass
+    displayName: 'FileUseViewer'
+
     propTypes: ->
         file_use_list : rtypes.array.isRequired
+        user_map      : rtypes.object.isRequired
+        project_map   : rtypes.object.isRequired

     render_list: ->
         for x in @props.file_use_list
-

-                {misc.to_json(x)}
-

+            <FileUse key={x.id}
+                id={x.id} project_id={x.project_id} path={x.path}
+                last_edited={x.last_edited} users={x.users}
+                user_map={@props.user_map} project_map={@props.project_map}/>

     render: ->

@@ -102,18 +142,21 @@

 FileUseController = rclass
+    displayName: 'FileUseController'
     propTypes: ->
-        flux     : rtypes.object
-        file_use : rtypes.object
+        flux        : rtypes.object
+        file_use    : rtypes.object
+        user_map    : rtypes.object
+        project_map : rtypes.object

     render: ->
-        if not @props.file_use? or not @props.flux?
+        if not @props.file_use? or not @props.flux? or not @props.user_map? or not @props.project_map?
             return <Loading/>
         file_use_list = @props.flux.getStore('file_use').get_sorted_file_use_list()
-        <FileUseViewer file_use_list={file_use_list}/>
+        <FileUseViewer file_use_list={file_use_list} user_map={@props.user_map} project_map={@props.project_map} />

 render = (flux) ->
-    <FluxComponent flux={flux} connectToStores={['file_use', 'users']} >
+    <FluxComponent flux={flux} connectToStores={['file_use', 'users', 'projects']} >
         <FileUseController />
     </FluxComponent>


file_use: autofocus search (old jquery for now)

--- salvus/page/file_use.cjsx
+++ salvus/page/file_use.cjsx
@@ -332,6 +332,7 @@
         $(window).resize(resize_notification_list)
         resize_notification_list()
         require('tasks').unset_key_handler()
+        notification_list.find("input").focus()
     else
         notification_list.hide()
         unbind_handlers()


make projects loading bigger

make projects loading bigger

--- salvus/page/projects.cjsx
+++ salvus/page/projects.cjsx
@@ -32,7 +32,7 @@
 {html_to_text} = require('misc_page')

 {Row, Col, Well, Button, ButtonGroup, ButtonToolbar, Grid, Input} = require('react-bootstrap')
-{ErrorDisplay, Icon, Saving, TimeAgo} = require('r_misc')
+{ErrorDisplay, Icon, Loading, Saving, TimeAgo} = require('r_misc')
 {React, Actions, Store, Table, flux, rtypes, rclass, FluxComponent}  = require('flux')
 {User} = require('users')

@@ -933,7 +933,9 @@

     render: ->
         if not @props.project_map? or not @props.user_map?
-            return
Loading...

+            return

+                <Loading />
+

         <Grid fluid className="constrained">
             <Well style={marginTop:'1em',overflow:'hidden'}>
                 <Row>


only 3 strategies for now. Sandbox, Scope and JS

--- src/client/embeds/deployment.coffee
+++ src/client/embeds/deployment.coffee
@@ -37,7 +37,7 @@
     # "_width" : "100%", //Dimensions to give the containing element. Passed as-is as style tag
     # "_height" : "50px", //Dimensions to give the containing element. Passed as-is as style tag

-    @settings._sandbox = 'raw'
+    @settings._sandbox = true
     @targets = @getTargets()
     @deploymentStrategy = @getDeploymentStrategy()
     @elements  = []
@@ -64,12 +64,12 @@

     DS = if @ship.index.match(/\.js$/)
       JSDeploymentStrategy
-    else if @settings._sandbox == 'raw'
-      RawDeploymentStrategy
-    else if !!@settings._sandbox
+    else if @settings._sandbox
       SandboxedDeploymentStrategy
     else
-      ScopedDeploymentStrategy
+      RawDeploymentStrategy
+    # else
+    #   ScopedDeploymentStrategy

     new DS(@)


Improved the test a bit

Improved the test a bit

--- test/streams/utils.coffee
+++ test/streams/utils.coffee
@@ -27,14 +27,21 @@
     source.pipe(piper1).pipe(piper2).pipe(dest)

     # Write to our source streams *out of order*
-    source1.end 'b'
-    source2.end 'c'
-    source.end  'a'
+    source1.write 'c'
+    source.write  'a'
+    source2.write 'e'
+    source1.write 'd'
+    source.write  'b'
+    source2.write 'f'
+
+    source.end()
+    source1.end()
+    source2.end()

     # And finally watch our writeable for data
     d = ''
     dest.on 'data', (chunk) -> d += chunk
     dest.on 'end',  ->
-      d.should.equal 'abc'
+      d.should.equal 'abcdef'
       done()


Fix id referencing. Fix state ref name. Remove some attribtues at the order.

--- src/coffee/orderdistribution.coffee
+++ src/coffee/orderdistribution.coffee
@@ -188,16 +188,21 @@

   replaceTaxCategories: (order, taxCategory) ->
     if order.shippingInfo? and order.shippingInfo.taxCategory?
-      order.shippingInfo.taxCategory[id] = taxCategory.id
+      order.shippingInfo.taxCategory['id'] = taxCategory.id

     order

   removeIdsAndVariantData: (order) ->
+    delete order.createdAt if order.createdAt?
+    delete order.lastModifiedAt if order.lastModifiedAt?
+    delete order.lastMessageSequenceNumber if order.lastMessageSequenceNumber?
+    delete order.syncInfo if order.syncInfo?
+
     _.each order.lineItems or [], (li) ->
       delete li.supplyChannel if li.supplyChannel?
       delete li.id if li.id?
       delete li.productId if li.productId?
-      delete li.states if li.states?
+      delete li.state if li.state?
       if li.variant?
         delete li.variant.id if li.variant.id?
         delete li.variant.attributes if li.variant.attributes?


Less verbose

Less verbose

--- src/coffee/orderdistribution.coffee
+++ src/coffee/orderdistribution.coffee
@@ -44,11 +44,11 @@
     ]).spread (masterChannelResult, retailerChannelResult) =>
       channelInMaster = masterChannelResult.body
       channelInRetailer = retailerChannelResult.body
-      @logger.info 'Channels ensured. About to process unsynced orders'
+      @logger.debug 'Channels ensured. About to process unsynced orders'

       @masterClient.orders.sort('id').last("#{@fetchHours}h").process (payload) =>
         unsyncedOrders = @_filterUnsyncedOrders payload.body.results, channelInMaster.id
-        @logger.info "About to distribute #{_.size unsyncedOrders} unsynced orders"
+        @logger.debug "About to distribute #{_.size unsyncedOrders} unsynced orders"
         @distributeOrders unsyncedOrders, channelInMaster, channelInRetailer
     .then =>
       if @summary.master.unsynced > 0
@@ -74,7 +74,7 @@
         @logger.error {badOrders: _.map(badOrders, (o) -> o.id)}, 'There are orders with different channels set!'

       # process orders sequentially
-      @logger.info "About to process #{_.size validOrders} valid orders"
+      @logger.debug "About to process #{_.size validOrders} valid orders"
       Qutils.processList validOrders, (orders) =>
         throw new Error 'Orders should be processed once at a time' if orders.length isnt 1
         order = orders[0]
@@ -113,7 +113,7 @@
           @retailerClient.orders.import(retailerOrder)
           .then (result) =>
             newOrder = result.body
-            @logger.info 'About to sync orders in master and retailer'
+            @logger.debug 'About to sync orders in master and retailer'

             @_updateSyncInfo(@masterClient, masterOrder.id, masterOrder.version, channelInMaster.id, newOrder.id)
             .then (syncInMaster) =>


Some Generator lifecycle tests

Some Generator lifecycle tests

--- spec/NetworkLifecycle.coffee
+++ spec/NetworkLifecycle.coffee
@@ -96,6 +96,42 @@
     output.sendDone
       out: "1#{first}:2#{second}:#{c.nodeId}"

+processGenerator = ->
+  c = new noflo.Component
+  c.inPorts.add 'start',
+    datatype: 'bang'
+  c.inPorts.add 'stop',
+    datatype: 'bang'
+  c.outPorts.add 'out',
+    datatype: 'bang'
+  c.autoOrdering = false
+
+  cleanUp = ->
+    clearInterval c.timer.interval
+    c.timer.deactivate()
+    c.timer = null
+
+  c.process (input, output, context) ->
+    if input.hasData 'start'
+      cleanUp() if c.timer
+      input.getData 'start'
+      c.timer = context
+      c.timer.interval = setInterval ->
+        output.send out: true
+      , 100
+    if input.hasData 'stop'
+      input.getData 'stop'
+      return output.done() unless c.timer
+      cleanUp()
+      output.done()
+
+  baseShutdown = c.shutdown.bind c
+  c.shutdown = ->
+    cleanUp() if c.timer
+    do baseShutdown
+
+  c
+
 describe 'Network Lifecycle', ->
   loader = null
   before (done) ->
@@ -106,6 +142,7 @@
       loader.registerComponent 'wirepattern', 'Merge', wirePatternMerge
       loader.registerComponent 'process', 'Async', processAsync
       loader.registerComponent 'process', 'Merge', processMerge
+      loader.registerComponent 'process', 'Generator', processGenerator
       loader.registerComponent 'legacy', 'Sync', legacyBasic
       done()

@@ -833,3 +870,60 @@
         in2.connect()
         in2.send 'foo'
         in2.disconnect()
+
+  describe 'with a Process API Generator component', ->
+    c = null
+    start = null
+    stop = null
+    out = null
+    before (done) ->
+      fbpData = "
+      INPORT=PcGen.START:START
+      INPORT=PcGen.STOP:STOP
+      OUTPORT=Pc.OUT:OUT
+      PcGen(process/Generator) OUT -> IN Pc(process/Async)
+      "
+      noflo.graph.loadFBP fbpData, (err, g) ->
+        return done err if err
+        loader.registerComponent 'scope', 'Connected', g
+        loader.load 'scope/Connected', (err, instance) ->
+          return done err if err
+          instance.once 'ready', ->
+            c = instance
+            start = noflo.internalSocket.createSocket()
+            c.inPorts.start.attach start
+            stop = noflo.internalSocket.createSocket()
+            c.inPorts.stop.attach stop
+            done()
+    beforeEach ->
+      out = noflo.internalSocket.createSocket()
+      c.outPorts.out.attach out
+    afterEach (done) ->
+      c.outPorts.out.detach out
+      out = null
+      c.shutdown done
+    it 'should not be running initially', ->
+      chai.expect(c.network.isRunning()).to.equal false
+    it 'should not be running even when network starts', (done) ->
+      c.start (err) ->
+        return done err if err
+        chai.expect(c.network.isRunning()).to.equal false
+        done()
+    it 'should start generating when receiving a start packet', (done) ->
+      c.start (err) ->
+        return done err if err
+        out.once 'data', ->
+          chai.expect(c.network.isRunning()).to.equal true
+          done()
+        start.send true
+    it 'should stop generating when receiving a stop packet', (done) ->
+      c.start (err) ->
+        return done err if err
+        out.once 'data', ->
+          chai.expect(c.network.isRunning()).to.equal true
+          stop.send true
+          setTimeout ->
+            chai.expect(c.network.isRunning()).to.equal false
+            done()
+          , 10
+        start.send true


Call TextEncoder constructor correctly.

Call TextEncoder constructor correctly.

--- test/unit/fileSpec.coffee
+++ test/unit/fileSpec.coffee
@@ -43,7 +43,7 @@
           mock = null

           beforeEach ->
-            encoder = TextEncoder('utf-8')
+            encoder = new TextEncoder('utf-8')
             # The mock plaintext file.
             encoded = encoder.encode('test data')
             mock =


Fix error where one too many dummy items were created

--- test/util/data-gen.coffee
+++ test/util/data-gen.coffee
@@ -24,7 +24,7 @@
   num = num or 100
   ret = []

-  for i in [0..num]
+  for i in [1..num]
     d1 = rand(past, now)
     d2 = rand(d1, now)


Removed reference gruntfile

Removed reference gruntfile

--- GruntFile.coffee
+++ GruntFile.coffee
@@ -1,400 +0,0 @@
-###*
- * Development tasks
- *
- * @author  Christopher Pappas
- * @date    2.3.14
- *
- * Primary Tasks:
- *    grunt dev   : Development mode, file-watcher
- *    grunt build : Minify all sources and deploy for distribution
- *
-###
-
-"use strict"
-
-
-handleify = require 'handleify'
-coffeeify = require 'coffeeify'
-uglifyify = require 'uglifyify'
-
-
-module.exports = (grunt) ->
-
-
-   # Root application path
-   basePath = '.'
-
-   # Directory where source files live
-   sources  = "#{basePath}/src"
-
-   # Directory where vendor files live
-   vendor   = "#{sources}/vendor"
-
-   # Path to compile to during development
-   output   = "#{basePath}/public"
-
-   # Path to build final deploy files
-   dist     = "#{basePath}/dist"
-
-   # Test specs directory and html
-   test     = "#{basePath}/test"
-
-   # Browser port during development
-   port     = 3000
-
-
-
-
-   grunt.initConfig
-
-
-      # --------------------------------------------------------
-      # Automatically prefix vendor-related CSS properties
-      # --------------------------------------------------------
-
-
-      'autoprefixer':
-
-         dev:
-            src:  "<%= sass.dev.files[0].dest %>"
-            dest: "<%= sass.dev.files[0].dest %>"
-
-         dist:
-            src:  "<%= sass.dist.files[0].dest %>"
-            dest: "<%= sass.dist.files[0].dest %>"
-
-
-
-      # --------------------------------------------------------
-      # Copy Bower sources to vendor directory for concat
-      # --------------------------------------------------------
-
-
-      'bower':
-
-         vendor:
-            dest: "#{vendor}"
-
-
-
-
-      # --------------------------------------------------------
-      # Compile JavaScript using Browserify
-      # --------------------------------------------------------
-
-
-      'browserify':
-
-         dev:
-            src: ["#{sources}/scripts/app.coffee"]
-            dest: "#{output}/assets/scripts/app.js"
-
-            options:
-               transform: [handleify, coffeeify]
-               debug: true
-
-         test:
-            src: ["#{test}/spec-runner.coffee"]
-            dest: "#{test}/html/spec/spec-runner.js"
-
-            options:
-               transform: "<%= browserify.dev.options.transform %>"
-               debug: "<%= browserify.dev.options.debug %>"
-
-         dist:
-            src: "<%= browserify.dev.src %>"
-            dest: "<%= browserify.dev.dest %>"
-
-            options:
-               transform: [handleify, coffeeify, uglifyify]
-               debug: false
-
-
-
-
-      # --------------------------------------------------------
-      # Concatinate Bower Vendor files copied from `grunt bower`
-      # --------------------------------------------------------
-
-
-      'concat':
-
-         options:
-            separator: ';'
-
-         vendor:
-            src: [
-               "#{vendor}/jquery.js"
-               "#{vendor}/lodash.js"
-               "#{vendor}/backbone.js"
-               "#{vendor}/greensock.js"
-            ]
-
-            dest: "#{output}/assets/scripts/vendor.js"
-
-
-
-
-      # --------------------------------------------------------
-      # Create server @ localhost:port and ip:port
-      # --------------------------------------------------------
-
-
-      'connect':
-
-         dev:
-            options:
-               hostname: null
-               port: "#{port}"
-               base: "#{output}"
-               livereload: true
-
-
-
-
-      # --------------------------------------------------------
-      # Copy assets {images|sound|etc} to output directory
-      # --------------------------------------------------------
-
-
-      'copy':
-
-         assets:
-            files: [{
-               expand: true
-               cwd: "#{sources}/assets"
-               src: ['**']
-               dest: "#{output}/assets"
-            }]
-
-         html:
-            files: [{
-               expand: true
-               cwd: "#{sources}/html"
-               src: ['**']
-               dest: "#{output}"
-            }]
-
-         dist:
-            files: [{
-               expand: true
-               cwd: "#{output}"
-               src:['**']
-               dest: "#{dist}"
-            }]
-
-
-
-
-      # --------------------------------------------------------
-      # Clean working development directory
-      # --------------------------------------------------------
-
-
-      'clean':
-
-         dev:
-            files: [{
-               expand: true
-               cwd: "#{output}"
-               src: ['**']
-            }]
-
-         dist:
-            files: [{
-               expand: true
-               cwd: "#{dist}"
-               src: ['**']
-            }]
-
-
-
-
-      # --------------------------------------------------------
-      # Optimize images using ImageMin
-      # --------------------------------------------------------
-
-
-      'imagemin':
-
-         dist:
-            options:
-               pngquant: true
-               optimizationLevel: 5
-
-            files: [{
-               expand: true
-               cwd: "#{output}/assets/images/"
-               src: ['**/*.{png,jpg,gif}']
-               dest: "#{output}/assets/images/"
-            }]
-
-
-
-
-      # --------------------------------------------------------
-      # Mocha tests via PhantomJS or the browser
-      # --------------------------------------------------------
-
-
-      'mocha':
-
-         test:
-            options:
-               reporter: 'Spec'
-               run: true
-               require: 'coffee-script'
-
-            mocha:
-               ignoreLeaks: true
-
-            src: ["#{test}/**/*.html"]
-
-
-
-
-      # --------------------------------------------------------
-      # Compile SASS source files
-      # --------------------------------------------------------
-
-
-      'sass':
-
-         dev:
-            options:
-               style: "expanded"
-               sourcemap: true
-
-            files: [{
-               src: ["#{sources}/styles/main.scss"]
-               dest: "#{output}/assets/styles/app.css"
-            }]
-
-         dist:
-            options:
-               style: "compressed"
-               sourcemap: false
-
-            files: [{
-               src: "#{sources}/styles/main.scss"
-               dest: "#{output}/assets/styles/app.css"
-            }]
-
-
-
-
-      # --------------------------------------------------------
-      # Watch files for changes and trigger appropriate tasks
-      # --------------------------------------------------------
-
-
-      'watch':
-
-         options:
-            livereload: true
-            spawn: false
-
-         assets:
-            files: "#{sources}/assets/**/*.*"
-            tasks: ['copy:assets']
-
-         html:
-            files: "#{sources}/html/**/*.*"
-            tasks: ['copy:html']
-
-         scripts:
-            files: "#{sources}/scripts/**/*.{js,coffee,hbs}"
-            tasks: ['browserify:dev']
-
-         styles:
-            files: "#{sources}/styles/**/*.{scss,sass}"
-            tasks: ['sass:dev', 'autoprefixer:dev']
-
-         test:
-           files: [ 'test/**/*.*' ]
-           tasks: [ 'test' ]
-
-         vendor:
-            files: "#{vendor}/**/*.js"
-            tasks: ['concat:vendor']
-
-
-
-
-      # --------------------------------------------------------
-      # Uglify vendor file
-      # --------------------------------------------------------
-
-
-      'uglify':
-
-         source:
-            src: "#{output}/assets/scripts/app.js"
-            dest: "#{output}/assets/scripts/app.js"
-
-         vendor:
-            src: "#{output}/assets/scripts/vendor.js"
-            dest: "#{output}/assets/scripts/vendor.js"
-
-
-
-   # + ----------------------------------------------------------
-
-
-
-   grunt.registerTask 'dev', [
-      'clean:dev'
-      'copy:assets'
-      'copy:html'
-      'browserify:dev'
-      'sass:dev'
-      'autoprefixer:dev'
-      'bower:vendor'
-      'concat:vendor'
-      'test'
-      'connect'
-      'watch'
-   ]
-
-
-   grunt.registerTask 'build', [
-      'clean:dev'
-      'clean:dist'
-      'copy:assets'
-      'copy:html'
-      'browserify:dist'
-      'sass:dist'
-      'autoprefixer:dist'
-      'bower:vendor'
-      'concat:vendor'
-      'uglify'
-      'imagemin'
-      'copy:dist'
-      'clean:dev'
-   ]
-
-
-   grunt.registerTask 'test', [
-      'browserify:dev'
-      'browserify:test'
-      'concat:vendor'
-      'mocha'
-   ]
-
-
-   grunt.registerTask 'default', ['dev']
-
-
-   # + ----------------------------------------------------------
-
-
-   # Load grunt dependencies based upon package.json
-   require('load-grunt-tasks')(grunt)
-
-   # Print stack traces on error
-   grunt.option 'stack', true
-
-   # Continue running grunt on error
-   grunt.option 'force', true
-
-


compile jade for directives

compile jade for directives

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -75,9 +75,9 @@
     # 'jscs'
     'coffeelint'
     'coffee:dev'
+    'jade:dev'
     'karma'
     'coffee:e2e'
-    'jade:dev'
     'express'
     'exec:e2e'
   ]
@@ -91,6 +91,7 @@
     # 'jscs'
     'coffeelint'
     'coffee:dev'
+    'jade:dev'
     'karma'
   ]


Watch for route code changes

Watch for route code changes

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -1,7 +1,7 @@
 module.exports = (grunt) ->

   coffeeFiles = [
-    "public/routes/*.coffee"
+    "routes/*.coffee"
     "src/coffee/*.coffee"
     "test/*.coffee"
     "app.coffee"


gruntfile: ensure css files are built when testing

encompass all pre-closureCompilation building into one place

previous commit d4260b7eefe5b1be0ec1762138a1368cfdf63e3a was reverted
for misunderstanding Gruntfile.coffee and breaking the build; this
commit gets it right

@ali01 @jbenet: I named this grunt task 'build', not sure that's the
right name though. Thoughts on a better one?

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -149,7 +149,7 @@
     # task to watch sources for changes and recompile during development
     watch:
       files: [paths.coffee_src, paths.less_src]
-      tasks: 'deps' # or 'test', or 'testserver' :)
+      tasks: 'build' # or 'test', or 'testserver' :)

     # task to run shell commands
     exec:
@@ -191,9 +191,9 @@
   grunt.loadNpmTasks 'grunt-jasmine-spec-server'

   # Register tasks
-  grunt.registerTask 'compile', ['exec:mkbuild', 'coffee', 'less:dev',
-                                 'closureCompiler']
+  grunt.registerTask 'compile', ['exec:mkbuild', 'build', 'closureCompiler']
   grunt.registerTask 'deps', ['coffee', 'closureDepsWriter']
-  grunt.registerTask 'test', ['clean', 'deps', 'jasmine', 'clean:test']
-  grunt.registerTask 'server', ['deps', 'jasmineSpecServer', 'watch']
+  grunt.registerTask 'build', ['deps', 'less:dev']
+  grunt.registerTask 'test', ['clean', 'build', 'jasmine', 'clean:test']
+  grunt.registerTask 'server', ['build', 'jasmineSpecServer', 'watch']
   grunt.registerTask 'default', ['compile']


Add sizereport to default task

Add sizereport to default task

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -168,4 +168,4 @@
   grunt.loadTasks "build/tasks"

   # set the default task.
-  grunt.registerTask "default", ["coffeelint","js","css","qunit"]
+  grunt.registerTask "default", ["coffeelint","js","css","qunit","sizereport"]


Integratenew coffee file in tasks

Integratenew coffee file in tasks

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -10,10 +10,12 @@
                     'index.js': 'dev/index.coffee'
                     'lib/bootstrapper.js': 'dev/lib/bootstrapper.coffee'
                     'lib/app.js': 'dev/lib/app.coffee'
+                    'lib/helper.js': 'dev/lib/helper.coffee'
                     'tests/index.js': 'dev/tests/index.coffee'
                     'tests/units/index.js': 'dev/tests/units/index.coffee'
                     'tests/units/lib/bootstrapper.js': 'dev/tests/units/lib/bootstrapper.coffee'
                     'tests/units/lib/app.js': 'dev/tests/units/lib/app.coffee'
+                    'tests/units/lib/helper.js': 'dev/tests/units/lib/helper.coffee'
         copy:
             dist:
                 files: [


fix Gruntfile syntax issue ><

fix Gruntfile syntax issue ><

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -173,9 +173,7 @@
     ]

   grunt.registerTask 'test:browser:sauce', () ->
-    if (process.env.TRAVIS
-      && process.env.CI
-      && process.env.TRAVIS_NODE_VERSION.indexOf("0.11") < 0)
+    if process.env.TRAVIS && process.env.CI && process.env.TRAVIS_NODE_VERSION.indexOf("0.11") < 0
       return
     grunt.task.run [
       'connect',


add karma task

add karma task

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -47,11 +47,16 @@
         files: html_sources
         tasks: ['templates', 'coffee']

+    karma:
+      unit:
+        configFile: 'karma.conf.coffee'
+
   grunt.initConfig config
   grunt.loadNpmTasks 'grunt-contrib-uglify'
   grunt.loadNpmTasks 'grunt-contrib-coffee'
   grunt.loadNpmTasks 'grunt-contrib-watch'
   grunt.loadNpmTasks 'grunt-contrib-connect'
+  grunt.loadNpmTasks 'grunt-karma'

   grunt.registerTask(
     'templates',


Include notify

Include notify

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -55,13 +55,14 @@

     jasmine:
       pivotal:
-        src: [
-          '<%=meta.bower%>/<%= meta.file %>.js']
+        src: '<%=meta.bower%>/<%= meta.file %>.js'
         options:
-          # vendor: 'spec/components/jquery/jquery.min.js'
           specs: '<%=meta.build%>/<%=meta.file%>.spec.js',
-          # outfile: 'spec.html'
-          # keepRunner: true
+
+    notify:
+      spec:
+        options: title: 'Quo Spec', message: 'Test 100% passed!'
+

     watch:
       core:
@@ -93,7 +94,7 @@
         tasks: ["coffee:style", "uglify:style"]
       spec:
         files: ['<%= source.spec %>']
-        tasks: ["concat:spec", "coffee:spec", "jasmine"]
+        tasks: ["concat:spec", "coffee:spec", "jasmine", "notify:spec"]

@@ -101,6 +102,7 @@
   grunt.loadNpmTasks "grunt-contrib-coffee"
   grunt.loadNpmTasks "grunt-contrib-uglify"
   grunt.loadNpmTasks "grunt-contrib-jasmine"
+  grunt.loadNpmTasks 'grunt-notify'
   grunt.loadNpmTasks "grunt-contrib-watch"

   # Default task.


Disables sourcemap

Disables sourcemap

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -11,6 +11,7 @@
       dist:
         options:
           bundleExec: true
+          sourcemap: "none" # remove that to generate sourcemap
         files: [{
           expand: true
           cwd: "sass/"
@@ -24,7 +25,8 @@
         files: [{
           expand: true
           cwd: "bower_components/fontawesome/css/"
-          src: [ "font-awesome.min.css", "font-awesome.css.map" ]
+          src: [ "font-awesome.min.css" ]
+          # src: [ "font-awesome.min.css", "font-awesome.css.map" ]
           dest: "assets/vendor/css/"
         },
         {
@@ -37,14 +39,16 @@
         files: [{
           expand: true
           cwd: "bower_components/jquery/dist/"
-          src: [ "jquery.min.js", "jquery.min.map" ]
+          src: [ "jquery.min.js" ]
+          # src: [ "jquery.min.js", "jquery.min.map" ]
           dest: "assets/vendor/js"
         }]
       bootstrap:
         files: [{
           expand: true
           cwd: "bower_components/bootstrap/dist/css/"
-          src: [ "bootstrap.min.css", "bootstrap.css.map" ]
+          src: [ "bootstrap.min.css" ]
+          # src: [ "bootstrap.min.css", "bootstrap.css.map" ]
           dest: "assets/vendor/css"
         },
         {


Concatenate and minifiy js files

Concatenate and minifiy js files

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -33,8 +33,22 @@
           dest: 'public/javascripts/'
           ext: '.js'
         ]
+    concat:
+      build:
+        dist:
+          src: ['public/javascripts/*.js']
+    uglify:
+      build:
+        files: [
+          expand: true
+          cwd: 'public/javascripts'
+          src: ['*.js']
+          dest: 'public/javascripts/'
+          ext: '.min.js'
+        ]

   grunt.loadNpmTasks 'grunt-contrib-less'
   grunt.loadNpmTasks 'grunt-contrib-cssmin'
+  grunt.loadNpmTasks 'grunt-contrib-concat'
   grunt.loadNpmTasks 'grunt-contrib-uglify'
   grunt.loadNpmTasks 'grunt-contrib-coffee'


add event listener to test importless example

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -8,14 +8,28 @@

 'use strict'

+importless = require 'importless'
+
 module.exports = (grunt) ->
+
+  # importless example: filter paths
+  grunt.event.on 'pathfinder-paths', (paths, setPaths, target) ->
+    if target is 'importless'
+      paths = importless.cssToLess paths
+      imported = importless.getImported paths
+      paths = importless.stripImported paths, imported
+      setPaths paths
+      grunt.log.writeln "Importless: Stripped imported file(s) #{imported} from paths"

   grunt.initConfig

     pathfinder:
-      main:
-        files: 'tmp/output.js': ['test/dir/**/*.js']
-        template: 'test/template.js'
+      requirejs:
+        files: 'tmp/requirejs_output.js': ['test/dir/**/*.js']
+        template: 'test/requirejs_template.js'
+      importless:
+        files: 'tmp/importless_output.less': ['test/dir/**/*.{css,less}']
+        template: 'test/importless_template.less'

     clean: ['tmp/']


Oops, no env var

Oops, no env var

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -6,7 +6,7 @@
 	grunt.initConfig
 		resourceToken: process.env['RESOURCE_TOKEN'] or 'http://vtex.io'
 		gitCommit: process.env['GIT_COMMIT'] or 'GIT_COMMIT'
-		deployDirectory: path.normalize(process.env['DEPLOY_DIRECTORY']) or path.resolve('.','deploy')
+		deployDirectory: path.normalize(process.env['DEPLOY_DIRECTORY'] ? 'deploy')
 		relativePath: ''
 		pkg: grunt.file.readJSON('package.json')
 		pacha: grunt.file.readJSON('tools/pachamama/pachamama.config')[0]


Accidentally duped repr function

Accidentally duped repr function

--- bokehjs/src/coffee/ticking/abstract_ticker.coffee
+++ bokehjs/src/coffee/ticking/abstract_ticker.coffee
@@ -3,33 +3,6 @@
   "backbone",
   "common/has_properties",
 ], (_, Backbone, HasProperties) ->
-
-  # A hacky analogue to repr() in Python.
-  repr = (obj) ->
-    if obj == null
-      return "null"
-
-    else if obj.constructor == Array
-      elems_str = (repr(elem) for elem in obj).join(", ")
-      return "[#{elems_str}]"
-
-    else if obj.constructor == Object
-      props_str = ("#{key}: #{repr(obj[key])}" for key of obj).join(", ")
-      return "{#{props_str}}"
-
-    else if obj.constructor == String
-      return "\"#{obj}\""
-
-    else if obj.constructor == Function
-      return "<Function: #{obj.name}>"
-
-    else
-      obj_as_string = obj.toString()
-      if obj_as_string == "[object Object]"
-        return "<#{obj.constructor.name}>"
-      else
-        return obj_as_string
-
   # A hacky analogue to repr() in Python.
   repr = (obj) ->
     if obj == null


Fixed autocomplete not working properly in variables

--- snippets/template.cson
+++ snippets/template.cson
@@ -57,54 +57,54 @@
 '.text.html.ss .variable':
   # Image manipulation 3.2
   'Scale Width Image 3.2+':
-    'prefix': 'scalewidth '
+    'prefix': 'scalewidth'
     'body': 'ScaleWidth(${1:width})${0}'
   'Scale Max Width Image 3.2+':
-    'prefix': 'scalemaxwidth '
+    'prefix': 'scalemaxwidth'
     'body': 'ScaleMaxWidth(${1:width})${0}'
   'Scale Height Image 3.2+':
-    'prefix': 'scaleheight '
+    'prefix': 'scaleheight'
     'body': 'ScaleHeight(${1:height})${0}'
   'Scale Max Height Image 3.2+':
-    'prefix': 'scalemaxheight '
+    'prefix': 'scalemaxheight'
     'body': 'ScaleMaxHeight(${1:height})${0}'
   'Fit Image 3.2+':
-    'prefix': 'fit '
+    'prefix': 'fit'
     'body': 'Fit(${1:width},${2:${1:height}})${0}'
   'Fit Max Image 3.2+':
-    'prefix': 'fitmax '
+    'prefix': 'fitmax'
     'body': 'FitMax(${1:width},${2:${1:height}})${0}'
   'Fill Image 3.2+':
-    'prefix': 'fill '
+    'prefix': 'fill'
     'body': 'Fill(${1:width},${2:${1:height}})${0}'
   'Fill Max Image 3.2+':
-    'prefix': 'fillmax '
+    'prefix': 'fillmax'
     'body': 'FillMax(${1:width},${2:${1:height}})${0}'
   'Crop Width Image 3.2+':
-    'prefix': 'cropwidth '
+    'prefix': 'cropwidth'
     'body': 'CropWidth(${1:width})${0}'
   'Crop Height Image 3.2+':
-    'prefix': 'cropheight '
+    'prefix': 'cropheight'
     'body': 'CropHeight(${1:width})${0}'
   'Pad Image 3.2+':
-    'prefix': 'pad '
+    'prefix': 'pad'
     'body': 'Pad(${1:width},${2:${1:height}},${3:hex})${0}'
   # Image manipulation 3.0 - 3.1
   'Set Width Image 3.0 - 3.1':
-    'prefix': 'setwidth '
+    'prefix': 'setwidth'
     'body': 'SetWidth(${1:width})${0}'
   'Set Height Image 3.0 - 3.1':
-    'prefix': 'setheight '
+    'prefix': 'setheight'
     'body': 'SetHeight(${1:height})${0}'
   'Set Size Image 3.0 - 3.1':
-    'prefix': 'setsize '
+    'prefix': 'setsize'
     'body': 'SetSize(${1:width},${2:${1:height}})${0}'
   'Set Ratio Size Image 3.0 - 3.1':
-    'prefix': 'setratiosize '
+    'prefix': 'setratiosize'
     'body': 'SetRatioSize(${1:width},${2:${1:height}})${0}'
   'Cropped Image 3.0 - 3.1':
-    'prefix': 'croppedimage '
+    'prefix': 'croppedimage'
     'body': 'CroppedImage(${1:width},${2:${1:height}})${0}'
   'Padded Image 3.0 - 3.1':
-    'prefix': 'paddedimage '
+    'prefix': 'paddedimage'
     'body': 'PaddedImage(${1:width},${2:${1:height}},${3:hex})${0}'


Options

Options

- add "cui-options--horiontal" class in "ng" design if parameter
  "horizontal" is set.

--- src/elements/Options/Options.coffee
+++ src/elements/Options/Options.coffee
@@ -413,6 +413,9 @@
 				@replace(bottom, "bottom")
 				@replace(top, "top")

+				if @_horizontal
+					@addClass("cui-options--horizontal")
+
 				if @_activatable
 					@empty("active")
 					@empty("inactive")


hide default overlay

hide default overlay

--- project/coffee/AppView.coffee
+++ project/coffee/AppView.coffee
@@ -101,7 +101,7 @@
         @B().router.start()
         @preloader.hide()

-        @B().openOverlayContent 'door_locks'
+        # @B().openOverlayContent 'door_locks'

         @updateMediaQueriesLog()
         return


Adds support for fn object notation to js object literal DSL

--- javascripts/lib/math/build_expression_from_javascript_object.coffee
+++ javascripts/lib/math/build_expression_from_javascript_object.coffee
@@ -23,6 +23,12 @@
           @processor,
           @component_builder)

+        @fn_converter = _FromFnObject.build(
+          @processor,
+          @component_builder
+        )
+
+
         @string_literal_converter = _FromStringLiteralObject.build(@component_builder,{
           "+": "build_addition"
           "-": "build_subtraction"
@@ -51,6 +57,7 @@
           @root_converter
           @variable_converter
           @fraction_converter
+          @fn_converter
           ]

       process: (js_object)->
@@ -197,8 +204,21 @@
           numerator: numerator
           denominator: denominator
         )
-
     class_mixer _FromFractionObject
+
+    class _FromFnObject
+      initialize: (@converter, @fn_builder)->
+      isType: (js_object)->
+        js_object['fn'] instanceof Array
+      convert: (js_object)->
+        name = js_object['fn'][0]
+        argument = convert_implicit_subexp(js_object['fn'][1], @converter)
+
+        @fn_builder.build_fn(
+          name: name
+          argument: argument
+        )
+    class_mixer _FromFnObject

     class _FromStringLiteralObject
       initialize: (@converter, @literal_mappings)->


Parameterize runtime name

Parameterize runtime name

--- source/compiler.coffee
+++ source/compiler.coffee
@@ -147,15 +147,16 @@

     @element tag, @contents(node)

-exports.compile = (parseTree, {compiler}={}) ->
+exports.compile = (parseTree, {compiler, runtime}={}) ->
   compiler ?= CoffeeScript
+  runtime ?= "require(\"hamlet-runtime\")"

   items = util.renderNodes(parseTree)

   source = """
     (data) ->
       (->
-        __runtime = Runtime(this)
+        __runtime = #{runtime}(this)

         __runtime.push document.createDocumentFragment()
     #{util.indent(items.join("\n"), "    ")}


Copy raw either

Copy raw either

--- docpad.coffee
+++ docpad.coffee
@@ -1,7 +1,5 @@
 # DocPad Configuration File
 # http://docpad.org/docs/config
-
-path = require 'path'

 #@poweredByDocPad = false

@@ -25,8 +23,10 @@
 @plugins =
   less:
     parseOptions:
-      paths: path.join __dirname, "node_modules"
+      paths: "#{__dirname}/node_modules"
   copy:
+    raw:
+      src: 'raw'
     fonts:
       src: '../node_modules/font-awesome/fonts'
       out: 'fonts'


fixed errors in specs

fixed errors in specs

--- spec/Extensions.coffee
+++ spec/Extensions.coffee
@@ -156,7 +156,9 @@
       grandchild = (parent) ->
         parent.tree('child')
         .deliver()
-        .maybe [abortion]
+        .then abortion
+        .else ->
+          true
         .then 'display-font', (c) ->
           c.registerAsset
             id: 'display-font-css'
@@ -167,6 +169,8 @@
         parent.tree('child')
         .deliver()
         .maybe [abortion]
+        .else ->
+          true
         .then grandchild
         .then 'ignored-font', (c) ->
           c.registerAsset
@@ -219,6 +223,8 @@
         parent.tree('winnergrandchild')
         .deliver()
         .maybe [abortion]
+        .else ->
+          true
         .then 'display-font', (c) ->
           c.registerAsset
             id: 'display-font-css'
@@ -229,6 +235,8 @@
         parent.tree('winnerchild')
         .deliver()
         .maybe [abortion]
+        .else ->
+          true
         .then winnergrandchild

       winner = (parent) ->
@@ -236,6 +244,8 @@
         .deliver()
         .all [winnerchild]
         .maybe [abortion]
+        .else ->
+          true
         .then 'ignore-comic-sans', (c) ->
           c.registerAsset
             id: 'display-font-css'


Remove comment

Remove comment

--- app/classifier/index.cjsx
+++ app/classifier/index.cjsx
@@ -41,7 +41,6 @@
     subject: null
     classification: null
     onLoad: Function.prototype
-    # cacheClassification: new CacheClassification

   getInitialState: ->
     backButtonWarning: false


updated new_home in zh-HANS.coffee

updated new_home in zh-HANS.coffee

--- app/locale/zh-HANS.coffee
+++ app/locale/zh-HANS.coffee
@@ -14,65 +14,65 @@
     for_developers: "适合开发者" # Not currently shown on home page.
     or_ipad: "或下载iPad版本"

-#  new_home:
-#    slogan: "The most engaging game for learning programming."
-#    classroom_edition: "Classroom Edition:"
-#    learn_to_code: "Learn to code:"
-#    teacher: "Teacher"
-#    student: "Student"
-#    play_now: "Play Now"
-#    im_a_teacher: "I'm a Teacher"
-#    im_a_student: "I'm a Student"
-#    learn_more: "Learn more"
-#    classroom_in_a_box: "A classroom in-a-box for teaching computer science."
-#    codecombat_is: "CodeCombat is a platform for students to learn computer science while playing through a real game."
-#    our_courses: "Our courses have been specifically playtested to excel in a classroom setting, even by teachers with little to no prior programming experience."
-#    designed_with: "Designed with teachers in mind"
-#    real_code: "Real, typed code"
-#    from_the_first_level: "from the first level"
-#    getting_students: "Getting students to typed code as quickly as possible is critical to learning programming syntax and proper structure."
-#    educator_resources: "Educator resources"
-#    course_guides: "and course guides"
-#    teaching_computer_science: "Teaching computer science does not require a costly degree, because we provide tools to support educators of all backgrounds."
-#    accessible_to: "Accessible to"
-#    everyone: "everyone"
-#    democratizing: "Democratizing the process of learning coding is at the core of our philosophy. Everyone should be able to learn to code."
-#    forgot_learning: "I think they actually forgot that they were actually learning something."
-#    wanted_to_do: " Coding is something I've always wanted to do, and I never thought I would be able to learn it in school."
-#    why_games: "Why is learning through games important?"
-#    games_reward: "Games reward the productive struggle."
-#    encourage: "Gaming is a medium that encourages interaction, discovery, and trial-and-error. A good game challenges the player to master skills over time, which is the same critical process students go through as they learn."
+  new_home:
+    slogan: "最引人入胜的编程游戏。"
+    classroom_edition: "教育版："
+    learn_to_code: "学习编程："
+    teacher: "教师"
+    student: "学生"
+    play_now: "开始游戏"
+    im_a_teacher: "我是老师"
+    im_a_student: "我是学生"
+    learn_more: "了解更多"
+    classroom_in_a_box: "教授计算机科学的一站式课堂。"
+    codecombat_is: "CodeCombat是一个使学生通过玩游戏来学习计算机科学的平台。"
+    our_courses: "我们的课程经历特殊测验，足够满足教学需求。无编程经验的教师也可掌控。"
+    designed_with: "从教育者角度设计"
+    real_code: "真实可行的代码"
+    from_the_first_level: "从第一关起"
+    getting_students: "让学生尽快动手写代码来学习程序语法和合适的结构是至关重要的。"
+    educator_resources: "教育资源"
+    course_guides: "和课程指导"
+    teaching_computer_science: "教授计算机科学不需要昂贵的学位，因为我们给不同背景的教育者予以支持。"
+    accessible_to: "提供给"
+    everyone: "每个人"
+    democratizing: "我们的核心理念是普及编程学习。每个人都应该有机会学习编程。"
+    forgot_learning: "我觉得他们甚至忘了自己在学东西。"
+    wanted_to_do: "写代码是我一直想做的事情。我从没想过我能在学校学习它。"
+    why_games: "为什么通过游戏来学习编程很重要？"
+    games_reward: "Games reward the productive struggle."
+    encourage: "游戏是鼓励交流、发现、试错的媒介。好的游戏能够让玩家学习到技能，正如学生在学习过程中所做的那样。"
 #    excel: "Games excel at rewarding"
 #    struggle: "productive struggle"
 #    kind_of_struggle: "the kind of struggle that results in learning that’s engaging and"
-#    motivating: "motivating"
-#    not_tedious: "not tedious."
-#    gaming_is_good: "Studies suggest gaming is good for children’s brains. (it’s true!)"
+    motivating: "富有激励性"
+    not_tedious: "而不单调乏味。"
+    gaming_is_good: "研究表明游戏有益儿童大脑。（这是真的！）"
 #    game_based: "When game-based learning systems are"
 #    compared: "compared"
 #    conventional: "against conventional assessment methods, the difference is clear: games are better at helping students retain knowledge, concentrate and"
-#    perform_at_higher_level: "perform at a higher level of achievement"
-#    feedback: "Games also provide real-time feedback that allows students to adjust their solution path and understand concepts more holistically, instead of being limited to just “correct” or “incorrect” answers."
-#    real_game: "A real game, played with real coding."
-#    great_game: "A great game is more than just badges and achievements - it’s about a player’s journey, well-designed puzzles, and the ability to tackle challenges with agency and confidence."
-#    agency: "CodeCombat is a game that gives players that agency and confidence with our robust typed code engine, which helps beginner and advanced students alike write proper, valid code."
-#    curious: "Curious? Request a demo and we'll show you the ropes"
-#    create_class: "Or create a class and see it for yourself!"
-#    request_demo: "Request a Demo"
-#    create_a_class: "Create a Class"
-#    have_an_account: "Already have an account?"
-#    logged_in_as: "You are currently logged in as"
-#    view_my_classes: "View my classes"
-#    computer_science: "Computer science courses for all ages"
-#    show_me_lesson_time: "Show me lesson time estimates for:"
-#    curriculum: "Total curriculum hours:"
-#    ffa: "Free for all students"
-#    lesson_time: "Lesson time:"
-#    coming_soon: "Coming soon!"
-#    courses_available_in: "Courses are available in JavaScript, Python, and Java (coming soon!)"
-#    boast: "Boasts riddles that are complex enough to fascinate gamers and coders alike."
-#    winning: "A winning combination of RPG gameplay and programming homework that pulls off making kid-friendly education legitimately enjoyable."
-#    run_class: "Everything you need to run a computer science class in your school today, no CS background required."
+    perform_at_higher_level: "perform at a higher level of achievement"
+    feedback: "游戏同时提供实时反馈，使得学生调整他们的解决方法，并且从整体理解概念，而不是局限于“正确”或者“错误”的答案。"
+    real_game: "一个真正的游戏，用真正的代码来玩耍。"
+    great_game: "一个伟大的游戏不仅仅是勋章和成就。它关于玩家的旅程，设计良好的关卡，以及用帮助和信心迎接挑战的能力。"
+    agency: "CodeCombat是一款这样的游戏：它通过我们的完善的代码引擎，给玩家提供帮助和信心，使得初学者和高级学生都可以编写合适、有效的代码。"
+    curious: "感兴趣吗？申请一个示例，我们将提供给你们诀窍。"
+    create_class: "或者创建一个班级，自己亲眼看一看！"
+    request_demo: "申请示例"
+    create_a_class: "创建班级"
+    have_an_account: "已有帐号？"
+    logged_in_as: "您现在的登录身份："
+    view_my_classes: "查看我的班级"
+    computer_science: "适合全年龄段的计算机科学课程"
+    show_me_lesson_time: "展示预估课程时间："
+    curriculum: "总计课程时间"
+    ffa: "对所有学生免费"
+    lesson_time: "课程时间："
+    coming_soon: "即将推出！"
+    courses_available_in: "课程可用如下语言：JavaScript，Python，还有Java（即将推出！）"
+    boast: "以难解的谜题为豪，它们复杂到可以吸引游戏玩家和程序员。"
+    winning: "RPG游戏和编程作业的完美结合，使得教育充满乐趣。"
+    run_class: "上计算机科学课程的所有东西都在这里，无需相关背景。"

   nav:
     play: "关卡选择" # The top nav bar entry where players choose which levels to play


Updatre PlatformVersionText

Updatre PlatformVersionText

--- app/models/project.coffee
+++ app/models/project.coffee
@@ -36,7 +36,7 @@
   versionText: (->
     platformVersion = @get "platformVersion"
     tNoPreference = @get "tNoPreference"
-    if Ember.isEmpty platformVersion
+    if platformVersion is 0
       tNoPreference
     else
       platformVersion


Add mono to keys-map.coffee

Add mono to keys-map.coffee

--- app/utils/keys-map.coffee
+++ app/utils/keys-map.coffee
@@ -19,6 +19,7 @@
   d:           'D'
   julia:       'Julia'
   csharp:      'C#'
+  mono:        'Mono'
   dart:        'Dart'
   elixir:      'Elixir'
 }


set groups

set groups

--- salvus/hub.coffee
+++ salvus/hub.coffee
@@ -2236,17 +2236,17 @@
     get_groups: (cb) =>
         # see note above about our "infinite caching".  Maybe a bad idea.
         if @groups?
-            cb(undefined, @groups)
+            cb?(undefined, @groups)
             return
         database.get_account
             columns    : ['groups']
             account_id : @account_id
             cb         : (err, r) =>
                 if err
-                    cb(err)
+                    cb?(err)
                 else
                     @groups = r['groups']
-                    cb(undefined, @groups)
+                    cb?(undefined, @groups)

     mesg_account_settings: (mesg) =>
         if @account_id != mesg.account_id


changeable colors may now be saved to localStorage

--- web-editor/app.coffee
+++ web-editor/app.coffee
@@ -16,6 +16,9 @@
 # - remove all use of <table> since <div> of <canvas> works nicely
 # - Use mouse to position cursor but then use KEYS to paint in the various
 #   colors, eliminates "mode" of selected color
+
+
+# - save changeable colors to localStorage

 # FEATURES
@@ -46,10 +49,17 @@
   # FIXME: Or load from localStorage:
   # When this object is constructed, the changeable color should either be a
   # default, or be pulled from localStorage for this character
-  id_for: {'charset':{}, 'sprites':{}}
-  for i in [0..255]
-    Color::id_for['charset'][i] = 10
-    Color::id_for['sprites'][i] = 10
+  prepare_id_for: ->
+    loaded = localStorage["changeable_colors"]
+    if loaded?
+      JSON.parse loaded
+    else
+      m = {'charset':{}, 'sprites':{}}
+      for i in [0..255]
+        m['charset'][i] = 10
+        m['sprites'][i] = 10
+      m
+  id_for: Color::prepare_id_for()

   # @param  hex  Example: '#68372B'
   constructor: ( @hex ) ->
@@ -749,7 +759,7 @@
       when K_3 then editor.choose_brush 3 if mode.color_mode is 'multi-color'
       when K_C then copy_from_index = selected_character_code
       when K_V then selected_character().copy_from  copy_from_index
-      when K_F then localStorage["data"] = JSON.stringify( data ); console.log 'Saved'
+      when K_F then localStorage['data'] = JSON.stringify( data ); localStorage['changeable_colors'] = JSON.stringify( Color::id_for ); console.log 'Saved'
       when K_L then data = JSON.parse localStorage["data"]; render_everything()
       when K_T then $('#tile_palette_dialog').fadeIn 'fast'
       when K_W then world.pan_view 'up'


- added line deletion (not just clearing) with ctrl+d

--- src/app/modules/editors/codeEditor/fileCodeView.coffee
+++ src/app/modules/editors/codeEditor/fileCodeView.coffee
@@ -63,7 +63,7 @@
         @editor.setHistory history
         #@editor.refresh()
         @updateUndoRedo()
-        @updateHints()
+        @_updateHints()
         @editor.focus()
       else
         @_clearMarkers()
@@ -113,7 +113,7 @@
             @editor.setOption("firstLineNumber",val)
             @render()
           when "linting"
-            @updateHints()
+            @_updateHints()

     _clearMarkers:=>
       @_markers = []
@@ -235,6 +235,12 @@
                 cm.indentSelection("add")
               else cm.replaceSelection("  ", "end")
           "Ctrl-Space": "autocomplete"
+          "Ctrl-D":(cm)->
+            console.log cm
+            doc = cm.getDoc()
+            line = doc.sel.anchor.line
+            cm.getDoc().removeLine(line)
+            return false

       @$el.attr('id', @model.name)
       @hlLine=  @editor.addLineClass(0, "activeline")


Moved the pagination class to the UL instead of the DIV

--- src/jquery-bootstrap-pagination.coffee
+++ src/jquery-bootstrap-pagination.coffee
@@ -151,8 +151,8 @@
       if @settings.total_pages is 1 and @settings.ignore_single_page
         return
       # render all of the pages:
-      html = ["
"]
-      html.push "
"
+      html = ["
"]
+      html.push "
"
       for link in @buildLinks()
         html.push link
       html.push "
"
@@ -197,4 +197,4 @@
       # current page:
       @render()

-)(jQuery)+)(jQuery)


Allow FormView to be submitted with by a field 'submit' event, such as when hitting enter in a text field.

--- lib/ST/FormView.coffee
+++ lib/ST/FormView.coffee
@@ -49,6 +49,7 @@
         else
           ST.TextFieldView.create()
       field.id attribute
+      field.bind 'submit', this
       field.searchRemotelyAt details.searchesRemotelyAt if details.searchesRemotelyAt
       @_fields[attribute] = field
       field.value(
@@ -86,6 +87,10 @@
     else
       @_model.createWithData(@data())
     @trigger 'saved', item
+
+  @method 'submit', ->
+    ST.command @_command, @method('save')
+    @_dialog.close() if @_dialog

   @method 'cancel', ->
     @trigger 'cancelled'
@@ -94,8 +99,6 @@
   @method 'dialogButtons', (dialog, buttonbar) ->
     @_dialog = dialog
     self = this
-    buttonbar.button '  OK  ', ->
-      ST.command self._command, self.method('save')
-      dialog.close()
+    buttonbar.button '  OK  ', @method('submit')
     buttonbar.button 'Cancel', @method('cancel')
     dialog.cancelFunction @method('cancel')


Compile arm

Compile arm

--- Gulpfile.coffee
+++ Gulpfile.coffee
@@ -65,5 +65,5 @@

 gulp.task 'dist', ['build'], ->
   gulp.src 'build/**'
-    .pipe electronPckg {version: '0.36.8', platform: 'linux'}
+    .pipe electronPckg {version: '0.36.8', platform: 'linux', arch: 'arm'}
     .pipe gulp.dest 'dist'


Gulp now ignoring jade files in src/renderer/scripts

--- Gulpfile.coffee
+++ Gulpfile.coffee
@@ -107,7 +107,7 @@
 # Jade Task
 #
 g.task "jade", ->
-    g.src genPaths("", "jade", ["!#{gulpOption.sourceDir}/scripts/**/*.jade"])
+    g.src genPaths("", "jade", ["!#{gulpOption.sourceDir}/renderer/scripts/**/*.jade"])
         .pipe $.plumber()
         .pipe $.changed("#{gulpOption.buildDir}/renderer/")
         .pipe $.jade()


Error message

Error message

--- lib/ajax_cursor.coffee
+++ lib/ajax_cursor.coffee
@@ -19,5 +19,5 @@
       .set('Accept', 'application/json')
       .end (err, res) =>
         return callback(err) if err
-        return callback(new Error "Ajax failed with status #{res.status}") unless res.ok
+        return callback(new Error "Ajax failed with status #{res.status} with: #{util.inspect(res.body)}") unless res.ok
         callback(null, if query.$count then res.body.result else res.body)


No need to guard .getCursor()

No need to guard .getCursor()

--- lib/copy-dialog.coffee
+++ lib/copy-dialog.coffee
@@ -34,8 +34,8 @@
             if not error?
               atom.workspace.open newPath,
                 activatePane: true
-                initialLine: activeEditor?.getCursor()?.getBufferRow() ? 0
-                initialColumn: activeEditor?.getCursor()?.getBufferColumn() ? 0
+                initialLine: activeEditor?.getCursor().getBufferRow()
+                initialColumn: activeEditor?.getCursor().getBufferColumn()
             else
               throw error
       if repo = atom.project.getRepo()


:art: Remove a redundant line

:art: Remove a redundant line

--- lib/linter-plus.coffee
+++ lib/linter-plus.coffee
@@ -7,7 +7,6 @@
 H = require './h'

 class Linter
-
   constructor: ->
     @subscriptions = new CompositeDisposable
     @lintOnFly = true


selector fixes

selector fixes

--- lib/ui/selector.coffee
+++ lib/ui/selector.coffee
@@ -3,6 +3,7 @@
 module.exports =
   show: (xs, f) ->
     @selector ?= new SelectListView
+    @selector.setItems []
     @selector.storeFocusedElement()
     @selector.viewForItem = (item) =>
       "<li>#{item}</li>"
@@ -15,14 +16,13 @@
       @selector.setItems xs

     panel = atom.workspace.addModalPanel(item: @selector)
-    panel.show()
     @selector.focusFilterEditor()

     confirmed = false
     @selector.confirmed = (item) =>
-      panel.destroy()
       f(item)
       confirmed = true
+      @selector.cancel()
     @selector.cancelled = =>
       panel.destroy()
       f() unless confirmed


fix line

fix line

--- src/Parser.coffee
+++ src/Parser.coffee
@@ -76,6 +76,7 @@
             table:  'table|tbody|thead|tfoot|tr|td|th'
         @hooks = {}
         @html = no
+        @line = no

         @blockParsers = [
             ['code', 10]
@@ -124,6 +125,9 @@

     enableHtml: (@html = yes) ->

+
+    enableLine: (@line = yes) ->
+

     hook: (type, cb) ->
         @hooks[type] = [] if not @hooks[type]?
@@ -205,6 +209,42 @@

         @holders = {} if clearHolders
         text
+
+
+    # mark line
+    markLine: (start, end = -1) ->
+        if @line
+            end = if end < 0 then start else end
+            return ''
+
+        ''
+
+
+    # mark lines
+    markLines: (lines, start) ->
+        i = -1
+
+        if @line then lines.map (line) =>
+            i += 1
+            start = start + i
+            (@markLine start) + line
+        else lines
+
+
+    # optimize lines
+    optimizeLines: (html) ->
+        last = 0
+
+        if @line then html.replace //, (matches...) ->
+            current = parseInt matches[1]
+            if current != last
+                replace = ''
+            else
+                replace = matches[0]
+
+            last = 1 + parseInt matches[2]
+            replace
+        else html

     # parse inline
@@ -698,7 +738,7 @@
         @call 'afterOptimizeBlocks', blocks, lines

-    parseCode: (lines, parts) ->
+    parseCode: (lines, parts, start) ->
         [blank, lang] = parts
         lang = trim lang
         count = blank.length
@@ -712,57 +752,62 @@
                 lang = trim lang
                 rel = trim rel

+        isEmpty = yes

         lines = lines.slice 1, -1
             .map (line) ->
-                line.replace (new RegExp "/^[ ]{#{count}}/"), ''
-
-        str = lines.join "\n"
-
-        if str.match /^\s*$/ then '' else '
<code' \
+                line = line.replace (new RegExp "/^[ ]{#{count}}/"), ''
+                if isEmpty and !line.match /^\s*$/
+                    isEmpty = no
+
+                htmlspecialchars line
+
+        str = (@markLines lines, start + 1).join "\n"
+
+        if isEmpty then '' else '
' \
-            + (htmlspecialchars str) + '
'
+            + str + '
'

-    parsePre: (lines) ->
+    parsePre: (lines, value, start) ->
         lines = lines.map (line) ->
             htmlspecialchars line.substring 4
-        str = lines.join "\n"
+        str = (@markLines lines, start).join "\n"

         if str.match /^\s*$/ then '' else '
' + str + '
'

-    parseAhtml: (lines) ->
-        trim lines.join "\n"
-
-
-    parseShtml: (lines) ->
-        trim (lines.slice 1, -1).join "\n"
-
-
-    parseMath: (lines) ->
-        '
' + (htmlspecialchars lines.join "\n") + '
'
-
-
-    parseSh: (lines, num) ->
-        line = @parseInline trim lines[0], '# '
+    parseAhtml: (lines, value, start) ->
+        trim (@markLines lines, start).join "\n"
+
+
+    parseShtml: (lines, value, start) ->
+        trim (@markLines (lines.slice 1, -1), start + 1).join "\n"
+
+
+    parseMath: (lines, value, start, end) ->
+        '
' + (@markLine start, end) + (htmlspecialchars lines.join "\n") + '
'
+
+
+    parseSh: (lines, num, start, end) ->
+        line = (@markLine start, end) + @parseInline trim lines[0], '# '
         if line.match /^\s*$/ then '' else "<h#{num}>#{line}</h#{num}>"

-    parseMh: (lines, num) ->
-        @parseSh lines, num
-
-
-    parseQuote: (lines) ->
+    parseMh: (lines, num, start, end) ->
+        @parseSh lines, num, start, end
+
+
+    parseQuote: (lines, value, start) ->
         lines = lines.map (line) ->
             line.replace /^\s*> ?/, ''
         str = lines.join "\n"

-        if str.match /^\s*$/ then '' else '
' + (@parse str) + '
'
-
-
-    parseList: (lines) ->
+        if str.match /^\s*$/ then '' else '
' + (@parse str, yes, start) + '
'
+
+
+    parseList: (lines, value, start) ->
         html = ''
         minSpace = 99999
         secondMinSpace = 99999
@@ -797,8 +842,9 @@

         lastType = ''
         leftLines = []
-
-        for row in rows
+        leftStart = 0
+
+        for row, key in rows
             if row instanceof Array
                 [space, type, line, text] = row

@@ -806,7 +852,7 @@
                     leftLines.push line.replace (new RegExp "^\\s{#{secondMinSpace}}"), ''
                 else
                     if leftLines.length > 0
-                        html += '
' + (@parse (leftLines.join "\n"), yes) + '
'
+                        html += '
' + (@parse (leftLines.join "\n"), yes, start + leftStart) + '
'

                     if lastType != type
                         if !!lastType
@@ -814,18 +860,19 @@

                         html += "<#{type}>"

+                    leftStart = key
                     leftLines = [text]
                     lastType = type
             else
                 leftLines.push row.replace (new RegExp "^\\s{#{secondMinSpace}}"), ''

         if leftLines.length > 0
-            html += '
' + (@parse (leftLines.join "\n"), yes) + "
</#{lastType}>"
+            html += '
' + (@parse (leftLines.join "\n"), yes, start + leftStart) + "
</#{lastType}>"

         html

-    parseTable: (lines, value) ->
+    parseTable: (lines, value, start) ->
         [ignores, aligns] = value
         head = ignores.length > 0 and (ignores.reduce (prev, curr) -> curr + prev) > 0

@@ -838,6 +885,8 @@
                 if head and output
                     head = no
                     body = yes
+
+                html += @markLine start + key
                 continue

             line = trim line
@@ -869,7 +918,7 @@
             else if body
                 html += '
'

-            html += '
'
+            html += '
' + @markLine start + key

             for key, column of columns
                 [num, text] = column
@@ -898,7 +947,8 @@

-    parseHr: -> '
'
+    parseHr: (lines, value, start) ->
+        if @line then '
' else '
'

     parseNormal: (lines) ->


Fix force for VM stop/restart.

Fix force for VM stop/restart.

--- src/api/vm.coffee
+++ src/api/vm.coffee
@@ -383,7 +383,9 @@
     # Attempts a clean reboot.
     xapi.call 'VM.clean_reboot', VM.ref
   catch error
-    throw error unless error[0] is 'VM_MISSING_PV_DRIVERS'
+    return unless error[0] is 'VM_MISSING_PV_DRIVERS'
+
+    @throw 'INVALID_PARAMS' unless force

     xapi.call 'VM.hard_reboot', VM.ref

@@ -430,7 +432,9 @@
     # Attempts a clean shutdown.
     xapi.call 'VM.clean_shutdown', VM.ref
   catch error
-    throw error unless error[0] is 'VM_MISSING_PV_DRIVERS'
+    return unless error[0] is 'VM_MISSING_PV_DRIVERS'
+
+    @throw 'INVALID_PARAMS' unless force

     xapi.call 'VM.hard_shutdown', VM.ref


fix "Uncaught ReferenceError: define is not defined"

--- src/client.coffee
+++ src/client.coffee
@@ -65,7 +65,7 @@
             else
                 enqueue data

-if define and define.amd
-    define? 'storage', [], d
+if typeof define is 'function' and define.amd
+    define 'storage', [], d
 else
     do d


Fix dupe functions

Fix dupe functions

--- src/cursor.coffee
+++ src/cursor.coffee
@@ -251,9 +251,9 @@
     column = @goalColumn if @goalColumn?
     @setScreenPosition({row: row + rowCount, column: column})
     @goalColumn = column
-  moveCursorUp: (rowCount=1, options) ->
-    deprecate("Use Cursor::moveUp() instead. `Cursor` has been removed from these methods.")
-    @moveUp(rowCount, options)
+  moveCursorDown: (rowCount=1, options) ->
+    deprecate("Use Cursor::moveDown() instead. `Cursor` has been removed from these methods.")
+    @moveDown(rowCount, options)

   # Public: Moves the cursor left one screen column.
   #
@@ -268,9 +268,9 @@
       {row, column} = @getScreenPosition()
       [row, column] = if column > 0 then [row, column - 1] else [row - 1, Infinity]
       @setScreenPosition({row, column})
-  moveCursorUp: (rowCount=1, options) ->
-    deprecate("Use Cursor::moveUp() instead. `Cursor` has been removed from these methods.")
-    @moveUp(rowCount, options)
+  moveCursorLeft: (rowCount=1, options) ->
+    deprecate("Use Cursor::moveLeft() instead. `Cursor` has been removed from these methods.")
+    @moveLeft(rowCount, options)

   # Public: Moves the cursor right one screen column.
   #


Update docker.coffee

Update docker.coffee

Use .. as build dir

--- src/docker.coffee
+++ src/docker.coffee
@@ -28,7 +28,7 @@
     @compose = {}
     createDockerFile("#{appdir}/.jakhu/#{tub}")
   image: (lang) ->
-    fs.appendFileSync @file, "FROM jakhu/#{lang}:latest\nRUN sudo chown -R jakhu /runner\nCOPY tub_config.yml ~/.jakhu/tub_config.yml\nCOPY ../.. /app"
+    fs.appendFileSync @file, "FROM jakhu/#{lang}:latest\nRUN sudo chown -R jakhu /runner\nCOPY tub_config.yml ~/.jakhu/tub_config.yml\nCOPY . /app\n"
     fs.appendFileSync @file, "RUN mkdir ~/.jakhu\nADD tub_config.yml /home/jakhu/.jakhu/tub_config.yml\nRUN sudo git clone https://github.com/Gum-Joe/jakhu-runner ~/.jakhu/runner\nVOLUME ../.. /app\n"
   cwd: () ->
     fs.appendFileSync @file, 'CMD bash -c "sudo chmod 777 ~/.jakhu/runner && source /home/jakhu/.rvm/scripts/rvm && rvm use ruby-head && ruby ~/.jakhu/runner/bin/jakhurun start"'
@@ -44,7 +44,8 @@
   # Create Compose YAML file
   f = 0
   while f < yml.tubs.length
-    @compose[yml.tubs[f]] = build: "#{yml.tubs[f]}"
+    @compose[yml.tubs[f]] = build: ".."
+    @compose[yml.tubs[f]] = dockerfile: "#{yml.tubs[f]}/Dockerfile"
     @compose[yml.tubs[f]].ports = []
     if yml.public
       # body...


Remove some metaprogrammed delegators and add docs

--- src/editor.coffee
+++ src/editor.coffee
@@ -69,10 +69,9 @@
   selections: null
   suppressSelectionMerging: false

-  @delegatesMethods 'foldAll', 'unfoldAll', 'foldAllAtIndentLevel', 'foldBufferRow',
-    'unfoldBufferRow', 'suggestedIndentForBufferRow', 'autoIndentBufferRow', 'autoIndentBufferRows',
+  @delegatesMethods 'suggestedIndentForBufferRow', 'autoIndentBufferRow', 'autoIndentBufferRows',
     'autoDecreaseIndentForBufferRow', 'toggleLineCommentForBufferRow', 'toggleLineCommentsForBufferRows',
-    'isFoldableAtBufferRow', toProperty: 'languageMode'
+    toProperty: 'languageMode'

   constructor: ({@softTabs, initialLine, tabLength, softWrap, @displayBuffer, buffer, registerEditor, suppressCursorCreation}) ->
     super
@@ -681,11 +680,57 @@
     bufferRow = @bufferPositionForScreenPosition(@getCursorScreenPosition()).row
     @unfoldBufferRow(bufferRow)

-  # Public: For each selection, fold all rows it intersects.
+  # For each selection, fold all rows it intersects.
+  #
+  # TODO: Rename to foldSelectedLines?
   foldSelection: ->
     selection.fold() for selection in @getSelections()

-  # {Delegates to: DisplayBuffer.createFold}
+  # Public: Fold all foldable lines.
+  foldAll: ->
+    @languageMode.foldAll()
+
+  # Public: Unfold all existing folds.
+  unfoldAll: ->
+    @languageMode.unfoldAll()
+
+  # Public: Fold all foldable lines at the given indent level.
+  #
+  # level - A {Number}.
+  foldAllAtIndentLevel: (level) ->
+    @languageMode.foldAllAtIndentLevel(level)
+
+  # Fold the given row in buffer coordinates based on its indentation level.
+  #
+  # If the given row is foldable, the fold will begin there. Otherwise, it will
+  # begin at the first foldable row preceding the given row.
+  #
+  # bufferRow - A {Number}.
+  #
+  # TODO: Make public when ::unfoldBufferRow goes public
+  foldBufferRow: (bufferRow) ->
+    @languageMode.foldBufferRow(bufferRow)
+
+  # Destroy the largest fold containing the given row in buffer
+  # coordinates.
+  #
+  # bufferRow - A {Number}
+  #
+  # TODO: Should this destroy all folds containing the given buffer row?
+  unfoldBufferRow: (bufferRow) ->
+    @languageMode.unfoldBufferRow(bufferRow)
+
+  # Public: Determine whether the given row in buffer coordinates is foldable.
+  #
+  # A *foldable* row is a row that *starts* a row range that can be folded.
+  #
+  # bufferRow - A {Number}
+  #
+  # Returns a {Boolean}.
+  isFoldableAtBufferRow: (bufferRow) ->
+    @languageMode.isFoldableAtBufferRow(bufferRow)
+
+  # TODO: Rename to foldRowRange?
   createFold: (startRow, endRow) ->
     @displayBuffer.createFold(startRow, endRow)


cancel open

cancel open

--- src/editor.coffee
+++ src/editor.coffee
@@ -58,6 +58,7 @@

   open: (path) ->
     path = App.openPanel() if not path
+    return if not path
     @filename = path

     if File.isDirectory @filename


Added preDraw engine event

Added preDraw engine event

--- src/engine.coffee
+++ src/engine.coffee
@@ -57,8 +57,20 @@
   ###

   ###*
-  Called after the engine draws on the canvas, you
-  wish to draw additional things to the canvas.
+  Called before the engine draws the game objects on the canvas.
+
+  The current camera transform is applied.
+
+  @name preDraw
+  @methodOf Engine#
+  @event
+  ###
+
+  ###*
+  Called after the engine draws on the canvas.
+
+  The current camera transform is not applied, you may
+  choose to apply it yourself using I.cameraTransform.

   @name draw
   @methodOf Engine#
@@ -95,6 +107,8 @@
       canvas.withTransform I.cameraTransform, (canvas) ->
         if I.backgroundColor
           canvas.fill(I.backgroundColor)
+
+        self.trigger "preDraw", canvas

         I.objects.invoke("draw", canvas)


Guard against fs.readdirSync() exception

--- src/lister.coffee
+++ src/lister.coffee
@@ -21,20 +21,28 @@
     catch e
       false

+  list: (directoryPath) ->
+    if @isDirectory(directoryPath)
+      try
+        fs.readdirSync(@atomModulesDirectory)
+      catch e
+        []
+    else
+      []
+
   listAtomPackagesDirectory: ->
     packages = []
-    if @isDirectory(@atomModulesDirectory)
-      for child in fs.readdirSync(@atomModulesDirectory)
-        manifest = path.join(@atomModulesDirectory, child, 'package.json')
-        continue unless @isFile(manifest)
-        try
-          packageJson = JSON.parse(fs.readFileSync(manifest, 'utf8'))
-        catch e
-          continue
+    for child in @list(@atomModulesDirectory)
+      manifest = path.join(@atomModulesDirectory, child, 'package.json')
+      continue unless @isFile(manifest)
+      try
+        packageJson = JSON.parse(fs.readFileSync(manifest, 'utf8'))
+      catch e
+        continue

-        name = packageJson.name ? child
-        version = packageJson.version ? '0.0.0'
-        packages.push({name, version})
+      name = packageJson.name ? child
+      version = packageJson.version ? '0.0.0'
+      packages.push({name, version})

     console.log @atomModulesDirectory
     for pack, index in packages


Make componentdir configurable via CLI options

--- src/msgflo.coffee
+++ src/msgflo.coffee
@@ -23,6 +23,7 @@
     .option('--broker <uri>', 'Broker address', String, '')
     .option('--ide <uri>', 'FBP IDE address', String, 'http://app.flowhub.io')
     .option('--library <FILE.json>', 'Library configuration file', String, 'package.json')
+    .option('--componentdir <dirname>', 'Library directory', String, 'participants')
     .option('--graph <file.json>', 'Initial graph to load', String, '')
     .option('--ignore [process]', "Don't set up these processes", collectArray, [])
     .option('--forward <stderr,stdout>', "Forward these streams from child", String, 'stderr,stdout')


* minior comments added

* minior comments added

--- src/object.coffee
+++ src/object.coffee
@@ -137,6 +137,7 @@
       else
         defineProperty aObjectValue, '$type', @
     aObjectValue
+  #TODO: maybe should use Attributes class and put this method into it.
   attrKeys: ->
     result = []
     for k,v of @attributes


Clean up some freestanding @'s in object.coffee.

--- src/object.coffee
+++ src/object.coffee
@@ -85,9 +85,9 @@

   # Make every subclass and their instances observable.
   Batman.mixin @prototype, ObjectFunctions, Batman.EventEmitter, Batman.Observable
-  Batman.mixin @,          ObjectFunctions, Batman.EventEmitter, Batman.Observable
+  Batman.mixin this,       ObjectFunctions, Batman.EventEmitter, Batman.Observable

-  @classMixin: -> Batman.mixin @, arguments...
+  @classMixin: -> Batman.mixin this, arguments...
   @mixin: -> @classMixin.apply @prototype, arguments
   mixin: @classMixin

@@ -103,17 +103,17 @@

   @singleton: (singletonMethodName="sharedInstance") ->
     @classAccessor singletonMethodName,
-      get: -> @["_#{singletonMethodName}"] ||= new @
+      get: -> @["_#{singletonMethodName}"] ||= new this

   @accessor '_batmanID', -> @_batmanID()

   constructor: (mixins...) ->
-    @_batman = new Batman._Batman(@)
+    @_batman = new Batman._Batman(this)
     @mixin mixins...

   counter = 0
   _batmanID: ->
-    @_batman.check(@)
+    @_batman.check(this)
     @_batman.id ?= counter++
     @_batman.id