Client: handle redirection

Client: handle redirection

--- client/activity/lib/activityappview.coffee
+++ client/activity/lib/activityappview.coffee
@@ -119,7 +119,19 @@
         when 'post'    then 'activity'
         else type
       socialapi.cacheable type_, slug, (err, data) =>
-        if err then router.handleNotFound router.getCurrentPath()
+        if err
+          # if channel is redirected to another
+          # handle here
+          if err.error = "moved_permanently"
+            res = try JSON.parse err.description
+            location="/Activity/Public"
+
+            if res.typeConstant isnt "group"
+              location="/Activity/Topic/#{err.rootName}"
+
+            window.location.href=location
+
+          router.handleNotFound router.getCurrentPath()
         else
           { typeConstant } = data
           index = if typeConstant is 'topic' then 2 else 0 # 2 because we put topics after #koding #changelog


heroku setup

heroku setup

--- web.coffee
+++ web.coffee
@@ -2,6 +2,6 @@
 express = require 'express'

 app = express()
-app.use express.logger 'dev'
+app.use express.morgan 'dev'
 app.use gzippo.staticGzip "#{__dirname}/dist"
 app.listen process.env.PORT || 5000


Applying new design and create a breadcrumb for payment process [WIP]

--- client/landing/Intro/views/teamplan.coffee
+++ client/landing/Intro/views/teamplan.coffee
@@ -43,7 +43,8 @@
     @summary.addSubView @title  = new KDCustomHTMLView tagName: "h4"
     @summary.addSubView @price  = new KDCustomHTMLView tagName: "h5"
     @summary.addSubView @buyNow = new KDButtonView
-      cssClass : "solid buy-now"
+      cssClass : "buy-now"
+      style    : "solid green"
       title    : "BUY NOW"
       callback : =>
         @emit "PlanSelected", "custom-plan", {


Bind confirm when deserializing panes

Bind confirm when deserializing panes

Signed-off-by: Max Brunsfeld <78036c9b69b887700d5846f26a788d53b201ffbb@github.com>

--- src/pane.coffee
+++ src/pane.coffee
@@ -14,7 +14,9 @@
   activeItem: undefined
   focused: false

-  @deserialize: (state, {deserializers, config, notifications, confirm}) ->
+  @deserialize: (state, atomEnvironment) ->
+    {deserializers, config, notifications} = atomEnvironment
+    confirm = atomEnvironment.confirm.bind(atomEnvironment)
     {items, activeItemURI, activeItemUri} = state
     activeItemURI ?= activeItemUri
     state.items = compact(items.map (itemState) -> deserializers.deserialize(itemState))


stylistics

stylistics

--- library/semantic/platings.coffee
+++ library/semantic/platings.coffee
@@ -70,7 +70,7 @@
         try this.forms.container.addClass "loading"
         @ewrong ?= "Please check the entered information"
         assert _.isObject data = try @forms.download yes
-        this.issuedSubmission data, (success, values) =>
+        this.dataSubmission data, (success, values) =>
             this.forms.container.removeClass "loading"
             assert values and _.isObject values or null
             @forms.upload values; @forms.messages @ewrong


Not sure why that was commented

Not sure why that was commented

--- test/disable.coffee
+++ test/disable.coffee
@@ -91,14 +91,14 @@
     window.formSubmitted = ->
       setTimeout ->
         ok !submit.disabled
-        # equal "Submit", submit.value
+        equal "Submit", submit.value
         start()
       , 0

     equal "", submit.value
     submit.click()
     ok submit.disabled
-    # equal "Submitting...", submit.value
+    equal "Submitting...", submit.value

   asyncTest "submit input is disabled and enabled on remote form", ->
     form = $("<form data-remote action='/echo?callback=formSubmitted' method=post><input type=submit value='Comment' data-disable-with='Commenting...'></form>")


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/metered.coffee
+++ test/metered.coffee
@@ -1,5 +1,5 @@
 module "Metered"

-test ""
+test "should exist"

 module()


adding support to vote for multiple options

--- src/poll.coffee
+++ src/poll.coffee
@@ -86,6 +86,47 @@
 		catch error
 			@robot.logger.error error

+    robot.respond /vote\s+(\d+,?)/i ->
+        try
+            votes = msg.match[1]
+            user = msg.envelope.user['name']
+            room = msg.message.room
+            options = @robot.brain.data.poll[room].options
+            @robot.logger.info "Adding new vote for #{@robot.brain.data.poll[room].topic} with #{Object.keys(options).length} options"
+
+            #check if the user already voted
+            @robot.logger.info "Adding a vote for #{vote} for #{user}"
+            for index, option of options
+                @robot.logger.info "Checking #{index}: #{option.text}"
+                if option.users != undefined
+                    @robot.logger.info "Some have voted for this one"
+                    option.users = option.users.filter (currentUser) -> currentUser isnt user
+
+            @robot.logger.info "Finishes removing user"
+            votes = votes.split(",")
+            for(i = 0; i < votes.length; ++i){
+                vote = votes[i]
+                if vote >= Object.keys(options).length
+                    @robot.logger.info "Invalid vote. #{vote} >= #{Object.keys(options).length}"
+                    msg.reply "Please vote for a valid option"
+                    return
+
+                selectedOption = @robot.brain.data.poll[room].options[vote]
+                if selectedOption.users != undefined
+                    @robot.logger.info "add user to this option since it's defined"
+                    selectedOption.users.push(user)
+                else
+                    @robot.logger.info "First vote, create new array"
+                    selectedOption.users = []
+                    selectedOption.users.push(user)
+            }
+
+            @robot.brain.save()
+            @robot.logger.info "Brain saved"
+            msg.reply "Thanks for your vote"
+        catch error
+            @robot.logger.error error
+
 	robot.respond /(view|show|poll) result(s)?/i, (msg) ->
 		try
 			@robot.logger.info "View results"


account: update last login time zone with each account changed event

--- client/Core/maincontroller.coffee
+++ client/Core/maincontroller.coffee
@@ -149,6 +149,15 @@
       KD.config.roles       = res.roles
       KD.config.permissions = res.permissions

+      KD.utils.getLocationInfo (err, result) ->
+        return warn err  if err
+        {timezone} = result
+
+        account.setTimezone lastLoginTimezone: timezone, (err) ->
+
+          warn err  if err
+
+
       @ready @emit.bind this, "AccountChanged", account, firstLoad

       @emit 'ready'


Main: code styling and emojify protection

--- client/Core/maincontroller.coffee
+++ client/Core/maincontroller.coffee
@@ -32,13 +32,13 @@

     @welcomeUser()

-    emojify.setConfig
-      img_dir : 'https://s3.amazonaws.com/koding-cdn/emojis'
-      ignored_tags  :
-        'TEXTAREA'  : 1,
-        'A'         : 1,
-        'PRE'       : 1,
-        'CODE'      : 1
+    emojify?.setConfig
+      img_dir      : 'https://s3.amazonaws.com/koding-cdn/emojis'
+      ignored_tags :
+        'TEXTAREA' : 1,
+        'A'        : 1,
+        'PRE'      : 1,
+        'CODE'     : 1

   welcomeUser : ->


mark ; and ⋄ as errors when not in the proper context

--- client/codemirror-apl-mode.coffee
+++ client/codemirror-apl-mode.coffee
@@ -83,12 +83,12 @@
         when '(' then a.push t: c, oi: la.oi, ii: la.ii; 'apl-par'
         when '[' then a.push t: c, oi: la.oi, ii: la.ii; 'apl-sqbr'
         when '{' then a.push t: c, oi: n, ii: n + INDENT_UNIT; "apl-dfn#{dfnDepth a} apl-dfn"
-        when ')' then (if '(' == la.t then a.pop(); 'apl-par'  else 'apl-err')
-        when ']' then (if '[' == la.t then a.pop(); 'apl-sqbr' else 'apl-err')
-        when '}' then (if '{' == la.t then a.pop(); "apl-dfn apl-dfn#{1 + dfnDepth a}" else 'apl-err')
-        when ';' then 'apl-semi'
-        when '⋄' then 'apl-diam'
-        when '⎕' then (if stream.match(/[áa-z0-9]*/i)?[0].toLowerCase() in quadNames then 'apl-quad' else 'apl-err')
+        when ')' then (if la.t == '(' then a.pop(); 'apl-par'  else 'apl-err')
+        when ']' then (if la.t == '[' then a.pop(); 'apl-sqbr' else 'apl-err')
+        when '}' then (if la.t == '{' then a.pop(); "apl-dfn apl-dfn#{1 + dfnDepth a}" else 'apl-err')
+        when ';' then la.t == '[' && 'apl-semi' || 'apl-err'
+        when '⋄' then la.t !in ['(', '['] && 'apl-diam' || 'apl-err'
+        when '⎕' then stream.match(/[áa-z0-9]*/i)?[0].toLowerCase() in quadNames && 'apl-quad' || 'apl-err'
         when '⍞' then 'apl-quad'
         when '#' then 'apl-ns'
         when '⍺','⍵','∇',':'


Leave lobby if close window

Leave lobby if close window

--- server/lobbysystem/lobbies.coffee
+++ server/lobbysystem/lobbies.coffee
@@ -1,3 +1,9 @@
+#Monitor user events
+Meteor.startup ->
+  Meteor.users.find({"status.online": false}).observeChanges
+    added: (id, fields)->
+      leaveLobby(id)
+
 setPlayerTeam = (lobby, uid, tteam)->
   return if !lobby?
   index = _.findWhere(lobby.radiant, {_id: uid})


fix the "report..." buttons

fix the "report..." buttons

--- js/inner.coffee
+++ js/inner.coffee
@@ -1347,7 +1347,7 @@
       return

     jq = intercomJQuery
-    jq("#IntercomDefaultWidget").click()
+    jq("#IntercomTab").click()
     unless jq('#IntercomNewMessageContainer').is(':visible')
       jq('.new_message').click()
     jq('#newMessageBody').focus()


sets running tests

sets running tests

--- components/split_test/running_tests.coffee
+++ components/split_test/running_tests.coffee
@@ -40,8 +40,8 @@

   artwork_item_contact_gallery:
     key: 'artwork_item_contact_gallery'
-    edge: 'default'
+    edge: 'contact'
     outcomes:
-      default: 1
-      contact: 0
+      default: 0.5
+      contact: 0.5
     dimension: 'dimension12'


From German image of card.

From German image of card.

--- coffeescripts/cards-common.coffee
+++ coffeescripts/cards-common.coffee
@@ -3210,7 +3210,7 @@
                 'Salvaged Astromech'
                 'Illicit'
             ]
-            points: 100
+            points: 27
         }
         {
             name: "Contrac???"


working on paymentHistory

working on paymentHistory

--- collections/paymentHistory.coffee
+++ collections/paymentHistory.coffee
@@ -34,6 +34,6 @@
     #                 value: user._id

 Payments.attachSchema(Schemas.Payments)
-Payments.after.insert (doc) ->
-  Meteor.call 'getTextMessage', doc, (e) ->
-  return
+
+Payments.before.insert (doc) ->
+  Meteor.call 'getTextMessage', (e) ->


kill mathjax console.log

kill mathjax console.log

--- shared/src/helpers/mathjax.coffee
+++ shared/src/helpers/mathjax.coffee
@@ -11,7 +11,6 @@
 # Search document for math and [data-math] elements and then typeset them
 typesetDocument = (windowImpl) ->
   latexNodes = []
-  console.log 'typesetting', windowImpl.document.querySelectorAll(MATH_DATA_SELECTOR)
   for node in windowImpl.document.querySelectorAll(MATH_DATA_SELECTOR)
     formula = node.getAttribute('data-math')
     # divs should be rendered as a block, others inline
@@ -25,7 +24,6 @@
     windowImpl.MathJax.Hub.Typeset(latexNodes)

   mathMLNodes = _.toArray(windowImpl.document.querySelectorAll(MATH_ML_SELECTOR))
-  console.log "MATML", mathMLNodes, windowImpl.document.innerHTML
   unless _.isEmpty(mathMLNodes)
     # style the entire document because mathjax is unable to style individual math elements
     windowImpl.MathJax.Hub.Typeset( windowImpl.document )
@@ -44,8 +42,6 @@
 # It's called by components like HTML after they're rendered
 typesetMath = (root, windowImpl = window) ->
   # schedule a Mathjax pass if there is at least one [data-math] or <math> element present
-  console.log 'render math', root.innerHTML, root.querySelector('math:not(.hild)')
-  debugger

   if windowImpl.MathJax?.Hub?.Queue? and root.querySelector(COMBINED_MATH_SELECTOR)
     typesetDocument(windowImpl)


feat(pages): add support for coffeescript blocks in asides

--- docs/js/pages.coffee
+++ docs/js/pages.coffee
@@ -17,6 +17,14 @@
     tocList.append "
#{content}
"

   $('hr').before toc
+
+  $('pre.coffeescript code').each ->
+    pre = $(this).parent()
+    coffee = pre.text()
+    code = hljs.highlight('ruby', coffee).value
+    pre.removeClass 'coffeescript'
+    $(this).addClass 'lang-coffeescript'
+    $(this).html code

   $('.lang-coffeescript').each ->
     pre = $(this).parent()


change Raven?.captureException to not log json, might be causing error

--- services/web/public/coffee/ide/chat/services/chatMessages.coffee
+++ services/web/public/coffee/ide/chat/services/chatMessages.coffee
@@ -51,9 +51,9 @@
 					if messages.length < MESSAGE_LIMIT
 						chat.state.atEnd = true
 					if !messages.reverse?
-						Raven?.captureException(new Error("messages has no reverse property #{JSON.stringify(messages)}"))
+						Raven?.captureException(new Error("messages has no reverse property #{typeof(messages)}"))
 					if typeof messages.reverse isnt 'function'
-						Raven?.captureException(new Error("messages.reverse not a function #{typeof(messages.reverse)} #{JSON.stringify(messages)}"))
+						Raven?.captureException(new Error("messages.reverse not a function #{typeof(messages.reverse)} #{typeof(messages)}"))
 						chat.state.errored = true
 					else
 						messages.reverse()


Remove the "command" API call from the docs. It's useless and we don't use it

--- resources/assets/js/outer/api_data.coffee
+++ resources/assets/js/outer/api_data.coffee
@@ -177,7 +177,6 @@
               "start_time" : "2013-02-12T21:33:38Z",
               "end_time" : "2013-02-12T21:33:39Z",
               "name" : "configure the build",
-              "command" : "configure the build",
               "exit_code" : null,
               "type" : "infrastructure",
               "index" : 0,
@@ -189,7 +188,6 @@
               "bash_command" : "lein2 deps",
               "run_time_millis" : 7555,
               "start_time" : "2013-02-12T21:33:47Z",
-              "command" : "((lein2 :deps))",
               "messages" : [ ],
               "step" : 1,
               "exit_code" : 0,
@@ -208,7 +206,6 @@
               "parallel" : true,
               "start_time" : "2013-02-12T21:33:59Z",
               "name" : "lein2 trampoline midje",
-              "command" : "((lein2 :trampoline :midje))",
               "messages" : [ ],
               "step" : 6,
               "exit_code" : 1,


recursive create_previews

recursive create_previews

--- scripts/create_previews.coffee
+++ scripts/create_previews.coffee
@@ -109,7 +109,10 @@

   Q.when null, ->
     if argv.path
-      albums.find(path: argv.path)
+      albums.find($or: [
+        { path: argv.path},
+        { 'breadcrumb.path': argv.path }
+      ])
     else
       albums.find()
   .then (albums) ->


JMachine: Add initScript field which is valid for every JMachines

--- workers/social/lib/social/models/computeproviders/machine.coffee
+++ workers/social/lib/social/models/computeproviders/machine.coffee
@@ -26,6 +26,9 @@
         type            : String
         default         : ""

+      initScript        :
+        type            : String
+
       users             : Array
       groups            : Array


ComputeProvider: Managed: we need to take into account the team plan while processing the managed vms

It was currently relying on user plan, but in team context it should take into account the team plan and also should update the team resource usage

--- workers/social/lib/social/models/computeproviders/managed.coffee
+++ workers/social/lib/social/models/computeproviders/managed.coffee
@@ -25,6 +25,36 @@
 getKiteIdOnly = (queryString) ->
   "///////#{queryString.split('/').reverse()[0]}"

+checkPlans = (options, callback) ->
+
+  { client, provider, change } = options
+  { r: { group, user, account } } = client
+
+  if group.slug is 'koding'
+
+    return callback null  if change is 'decrement'
+
+    { fetchUserPlan, fetchUsage } = require './computeutils'
+
+    fetchUserPlan client, (err, userPlan) ->
+      fetchUsage client, { provider }, (err, usage) ->
+        return callback err  if err?
+
+        if usage.total >= userPlan.managed
+          return callback new KodingError """
+            Total limit of #{userPlan.managed}
+            managed vm limit has been reached.
+          """, 'UsageLimitReached'
+
+        callback null
+
+  else
+
+    ComputeProvider = require './computeprovider'
+    ComputeProvider.updateGroupInstanceUsage group, change, 1, (err) ->
+      callback err
+
+
 module.exports = class Managed extends ProviderInterface

   @providerSlug = 'managed'
@@ -46,29 +76,21 @@
     queryString = getKiteIdOnly queryString
     provider    = @providerSlug

-    { guessNextLabel, fetchUserPlan, fetchUsage } = require './computeutils'
+    { guessNextLabel } = require './computeutils'

     guessNextLabel { user, group, label, provider }, (err, label) ->
-      fetchUserPlan client, (err, userPlan) ->
-        fetchUsage client, { provider }, (err, usage) ->
+      checkPlans { client, provider, change: 'increment' }, (err) ->
+        return callback err  if err?

-          return callback err  if err?
+        meta =
+          type          : Managed.providerSlug
+          storage_size  : 0 # sky is the limit.
+          alwaysOn      : no

-          if usage.total >= userPlan.managed
-            return callback new KodingError """
-              Total limit of #{userPlan.managed}
-              managed vm limit has been reached.
-            """, 'UsageLimitReached'
-
-          meta =
-            type          : Managed.providerSlug
-            storage_size  : 0 # sky is the limit.
-            alwaysOn      : no
-
-          callback null, {
-            meta, label, credential: client.r.user.username
-            postCreateOptions: { queryString, ipAddress }
-          }
+        callback null, {
+          meta, label, credential: client.r.user.username
+          postCreateOptions: { queryString, ipAddress }
+        }

   @postCreate = (client, options, callback) ->


Remove query string after requested model id set (#3273)

--- angular/core/components/thread_page/thread_page_controller.coffee
+++ angular/core/components/thread_page/thread_page_controller.coffee
@@ -3,6 +3,7 @@

   @requestedProposalKey = $routeParams.proposal or $location.search().proposal
   @requestedCommentId   = parseInt($routeParams.comment or $location.search().comment)
+  $location.url($location.path())

   handleCommentHash = do ->
     if match = $location.hash().match /comment-(\d+)/


Provide ticket sidebar with tags and links from origin all=true ticket request.

--- app/assets/javascripts/app/controllers/ticket_zoom/sidebar.coffee
+++ app/assets/javascripts/app/controllers/ticket_zoom/sidebar.coffee
@@ -27,6 +27,8 @@
           taskGet:  @taskGet
           formMeta: @formMeta
           markForm: @markForm
+          tags:     @tags
+          links:    @links
         )
       else
         @sidebarBackends[key].reload(
@@ -34,6 +36,8 @@
           query:   @query
           formMeta: @formMeta
           markForm: @markForm
+          tags:     @tags
+          links:    @links
         )
       item = @sidebarBackends[key].sidebarItem()
       if item


Block childNode parsing on SCRIPT and STYLE nodes. [#256]

--- src/view.coffee
+++ src/view.coffee
@@ -57,8 +57,6 @@
       @bindings.push new Rivets[binding] @, node, type, keypath, options

     parse = (node) =>
-      block = false
-
       if node.nodeType is 3
         parser = Rivets.TextTemplateParser

@@ -72,32 +70,34 @@
                 if token.type is 1
                   buildBinding 'TextBinding', text, null, token.value
               node.parentNode.removeChild node
-      else if componentRegExp.test node.tagName
-        type = node.tagName.replace(componentRegExp, '').toLowerCase()
-        @bindings.push new Rivets.ComponentBinding @, node, type
+      else if node.nodeType is 1
+        if componentRegExp.test node.nodeName
+          type = node.nodeName.replace(componentRegExp, '').toLowerCase()
+          @bindings.push new Rivets.ComponentBinding @, node, type
+        else
+          block = node.nodeName is 'SCRIPT' or node.nodeName is 'STYLE'

-      else if node.attributes?
-        for attribute in node.attributes
-          if bindingRegExp.test attribute.name
-            type = attribute.name.replace bindingRegExp, ''
+          for attribute in node.attributes
+            if bindingRegExp.test attribute.name
+              type = attribute.name.replace bindingRegExp, ''

-            unless binder = @binders[type]
-              for identifier, value of @binders
-                if identifier isnt '*' and identifier.indexOf('*') isnt -1
-                  regexp = new RegExp "^#{identifier.replace('*', '.+')}$"
-                  if regexp.test type
-                    binder = value
+              unless binder = @binders[type]
+                for identifier, value of @binders
+                  if identifier isnt '*' and identifier.indexOf('*') isnt -1
+                    regexp = new RegExp "^#{identifier.replace('*', '.+')}$"
+                    if regexp.test type
+                      binder = value

-            binder or= @binders['*']
+              binder or= @binders['*']

-            if binder.block
-              block = true
-              attributes = [attribute]
+              if binder.block
+                block = true
+                attributes = [attribute]

-        for attribute in attributes or node.attributes
-          if bindingRegExp.test attribute.name
-            type = attribute.name.replace bindingRegExp, ''
-            buildBinding 'Binding', node, type, attribute.value
+          for attribute in attributes or node.attributes
+            if bindingRegExp.test attribute.name
+              type = attribute.name.replace bindingRegExp, ''
+              buildBinding 'Binding', node, type, attribute.value

       unless block
         parse childNode for childNode in (n for n in node.childNodes)


Fix the data parameter construction

Fix the data parameter construction

--- cilantro/static/cilantro/scripts/coffeescript/views/charts.coffee
+++ cilantro/static/cilantro/scripts/coffeescript/views/charts.coffee
@@ -215,7 +215,12 @@
             if @model then @model.destroy()

         renderChart: (url, data, fields, seriesIdx) ->
-            data = _.extend {}, data, @options.data
+            if @options.data
+                for key, value of @options.data
+                    if not data
+                        data = "#{key}=#{value}"
+                    else
+                        data = data + "&#{key}=#{value}"
             Backbone.ajax
                 url: url
                 data: data
@@ -247,18 +252,16 @@
                 @$renderArea.addClass 'loading'
                 url = urlTmpl id: xAxis.id

-                fields = []
-                data = ''
-
-                if xAxis
-                    fields.push xAxis
-                    data += 'dimension=' + xAxis.id + '&'
+                fields = [xAxis]
+                data = 'dimension=' + xAxis.id
+
                 if yAxis
                     fields.push yAxis
-                    data += 'dimension=' + yAxis.id + '&'
+                    data = data + '&dimension=' + yAxis.id
+
                 if series
                     seriesIdx = if yAxis then 2 else 1
-                    data += 'dimension=' + series.id
+                    data = data + '&dimension=' + series.id

                 if event and @model
                     @model.set


ApplicationTabView: Make sure there is handle (since its optional this check is necessary)

--- client/Main/CommonViews/applicationview/applicationtabview.coffee
+++ client/Main/CommonViews/applicationview/applicationtabview.coffee
@@ -31,9 +31,9 @@
       {tabHandle}  = pane
       {plusHandle} = @getOptions().tabHandleContainer
       tabHandle.on "DragInAction", =>
-        plusHandle.hide() if tabHandle.dragIsAllowed
+        plusHandle?.hide() if tabHandle.dragIsAllowed
       tabHandle.on "DragFinished", =>
-        plusHandle.show()
+        plusHandle?.show()

     @on "SaveSessionData", (data) =>
       @appStorage.setValue "sessions", data if @isSessionEnabled
@@ -42,7 +42,7 @@
       if mainView = pane.getMainView()
         {tabView} = pane.getMainView()
         if this is tabView
-          @getActivePane()?.getHandle?().$().click()
+          @getActivePane()?.getHandle()?.$().click()

     mainView = KD.getSingleton("mainViewController").getView()
     mainView.mainTabView.on "PaneDidShow", focusActivePane


handle when plans overlap indirectly/through another plan, and also handle when the display start order is different from the order sent by the backend

--- tutor/src/components/course-calendar/duration.cjsx
+++ tutor/src/components/course-calendar/duration.cjsx
@@ -76,8 +76,36 @@

     groupedDurations = _.chain(groupingDurations)
       .map(@groupByRanges(durationsInView))
+      .each(@calcDurationHeight)
       .tap(@calcTopOffset)
       .value()
+
+  calcDurationHeight: (rangeData) ->
+    planGroups = _.reduce(rangeData.plansByDays, (groups, plansByDay) ->
+
+      planIds = _.map(plansByDay, (planInfo) -> planInfo.plan.id)
+
+      # if plans don't group together on adjacent days,
+      if _.isEmpty(_.intersection(_.last(groups), planIds))
+        # push as new group
+        groups.push(planIds)
+      else
+        # else, plans share adjacent days, group together.
+        groups[groups.length - 1] = _.union(_.last(groups), planIds)
+
+      groups
+
+    , [[]])
+
+    rangeData.maxPlansOnDay = _.max(planGroups, (plansInGroup) ->
+      plansInGroup.length
+    ).length
+
+    # set day height to the best-guess for this range based on how many plans it has.
+    # It'll be fine-tuned later across all ranges
+    rangeData.dayHeight = Math.max(
+      @_calcDayHeight(rangeData.maxPlansOnDay), rangeData.dayHeight
+    )

   calcTopOffset: (ranges) ->
     dayHeights = _.pluck(ranges, 'dayHeight')
@@ -100,7 +128,7 @@

         _.chain(plans)
           .sortBy((plan) ->
-            plan.rangeDuration.count('days')
+            -1 * plan.rangeDuration.start.valueOf()
           )
           .each(@setPlanOrder({current, existingOrdered, weekTopOffset, maxPlansOnDay}))
           .value()
@@ -244,15 +272,6 @@
         )
         rangeData.plansByDays.push(plansOnDay)

-      rangeData.maxPlansOnDay = _.max(rangeData.plansByDays, (plansOnDay) ->
-        plansOnDay.length
-      ).length
-
-      # set day height to the best-guess for this range based on how many plans it has.
-      # It'll be fine-tuned later across all ranges
-      rangeData.dayHeight = Math.max(
-        @_calcDayHeight(rangeData.maxPlansOnDay), rangeData.dayHeight
-      )
       rangeData


fix deploy spec to check for correct md5 sum in generated output

--- spec/core/deploy.spec.coffee
+++ spec/core/deploy.spec.coffee
@@ -37,7 +37,7 @@
   it "creates dist/config.ios.xml", =>
     iosConfigCreated = false
     waitsFor =>
-      iosConfigCreated = @session.stdout.match("Creating www/config.ios.xml")
+      iosConfigCreated = @session.stdout.match("Creating dist/config.ios.xml")

     runs ->
       expect( iosConfigCreated ).toBeTruthy()
@@ -45,7 +45,7 @@
   it "creates dist/config.android.xml", =>
     androidConfigCreated = false
     waitsFor =>
-      androidConfigCreated = @session.stdout.match("Creating www/config.android.xml")
+      androidConfigCreated = @session.stdout.match("Creating dist/config.android.xml")

     runs ->
       expect( androidConfigCreated ).toBeTruthy()


:tulip: spec gardening

:tulip: spec gardening

--- spec/text-editor-spec.coffee
+++ spec/text-editor-spec.coffee
@@ -1913,33 +1913,78 @@

   describe "buffer manipulation", ->
     describe ".moveLineUp", ->
-      describe "when there is only one line selected", ->
-        it "moves the line up by one row", ->
-          editor.setSelectedBufferRange([[3, 2], [3, 9]])
-          expect(editor.getSelectedBufferRange()).toEqual [[3, 2], [3, 9]]
-
-          editor.moveLineUp()
-
-          expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [2, 9]]
-
-      describe "when there is multiple selections", ->
-        it "moves the selected lines up by one row", ->
-          editor.setSelectedBufferRanges([[[1, 2], [1, 9]], [[3, 2], [3, 9]], [[5, 2], [5, 9]]])
-          expect(editor.getSelectedBufferRanges()).toEqual [[[1, 2], [1, 9]], [[3, 2], [3, 9]], [[5, 2], [5, 9]]]
-
-          editor.moveLineUp()
-
-          expect(editor.getSelectedBufferRanges()).toEqual [[[0, 2], [0, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]]
-
-        describe "when there is multiple lines selected and moved upward until the top-most line is at row 0", ->
-          it "moves all the lines upward until the top-most is at row 0, then no more lines are moved upward", ->
+      describe "when there is a single selection", ->
+        describe "when the selection spans a single line", ->
+          it "moves the line to the preceding row", ->
+            expect(editor.lineTextForBufferRow(2)).toBe "    if (items.length <= 1) return items;"
+            expect(editor.lineTextForBufferRow(3)).toBe "    var pivot = items.shift(), current, left = [], right = [];"
+
+            editor.setSelectedBufferRange([[3, 2], [3, 9]])
+            editor.moveLineUp()
+
+            expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [2, 9]]
+            expect(editor.lineTextForBufferRow(2)).toBe "    var pivot = items.shift(), current, left = [], right = [];"
+            expect(editor.lineTextForBufferRow(3)).toBe "    if (items.length <= 1) return items;"
+
+        describe "when the selection spans multiple lines", ->
+          it "moves the lines spanned by the selection to the preceding row", ->
+            expect(editor.lineTextForBufferRow(2)).toBe "    if (items.length <= 1) return items;"
+            expect(editor.lineTextForBufferRow(3)).toBe "    var pivot = items.shift(), current, left = [], right = [];"
+            expect(editor.lineTextForBufferRow(4)).toBe "    while(items.length > 0) {"
+
+            editor.setSelectedBufferRange([[3, 2], [4, 9]])
+            editor.moveLineUp()
+
+            expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [3, 9]]
+            expect(editor.lineTextForBufferRow(2)).toBe "    var pivot = items.shift(), current, left = [], right = [];"
+            expect(editor.lineTextForBufferRow(3)).toBe "    while(items.length > 0) {"
+            expect(editor.lineTextForBufferRow(4)).toBe "    if (items.length <= 1) return items;"
+
+        describe "when the selection spans multiple lines, but ends at column 0", ->
+          it "does not move the last line of the selection", ->
+            expect(editor.lineTextForBufferRow(2)).toBe "    if (items.length <= 1) return items;"
+            expect(editor.lineTextForBufferRow(3)).toBe "    var pivot = items.shift(), current, left = [], right = [];"
+            expect(editor.lineTextForBufferRow(4)).toBe "    while(items.length > 0) {"
+
+            editor.setSelectedBufferRange([[3, 2], [4, 0]])
+            editor.moveLineUp()
+
+            expect(editor.getSelectedBufferRange()).toEqual [[2, 2], [3, 0]]
+            expect(editor.lineTextForBufferRow(2)).toBe "    var pivot = items.shift(), current, left = [], right = [];"
+            expect(editor.lineTextForBufferRow(3)).toBe "    if (items.length <= 1) return items;"
+            expect(editor.lineTextForBufferRow(4)).toBe "    while(items.length > 0) {"
+
+      describe "when there are multiple selections", ->
+        describe "when all the selections span different lines", ->
+          it "moves all lines that are spanned by a selection to the preceding row", ->
             editor.setSelectedBufferRanges([[[1, 2], [1, 9]], [[3, 2], [3, 9]], [[5, 2], [5, 9]]])
-            expect(editor.getSelectedBufferRanges()).toEqual [[[1, 2], [1, 9]], [[3, 2], [3, 9]], [[5, 2], [5, 9]]]
-
             editor.moveLineUp()
+
+            expect(editor.getSelectedBufferRanges()).toEqual [[[0, 2], [0, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]]
+            expect(editor.lineTextForBufferRow(0)).toBe "  var sort = function(items) {"
+            expect(editor.lineTextForBufferRow(1)).toBe "var quicksort = function () {"
+            expect(editor.lineTextForBufferRow(2)).toBe "    var pivot = items.shift(), current, left = [], right = [];"
+            expect(editor.lineTextForBufferRow(3)).toBe "    if (items.length <= 1) return items;"
+            expect(editor.lineTextForBufferRow(4)).toBe "      current = items.shift();"
+            expect(editor.lineTextForBufferRow(5)).toBe "    while(items.length > 0) {"
+
+        describe "when one of the selections spans line 0", ->
+          it "doesn't move any lines, since line 0 can't move", ->
+            editor.setSelectedBufferRanges([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]])
+
             editor.moveLineUp()

-            expect(editor.getSelectedBufferRanges()).toEqual [[[0, 2], [0, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]]
+            expect(editor.getSelectedBufferRanges()).toEqual [[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[4, 2], [4, 9]]]
+            expect(buffer.isModified()).toBe false
+
+        describe "when one of the selections spans the last line, and it is empty", ->
+          it "doesn't move any lines, since the last line can't move", ->
+            buffer.append('\n')
+            editor.setSelectedBufferRanges([[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[13, 0], [13, 0]]])
+
+            editor.moveLineUp()
+
+            expect(editor.getSelectedBufferRanges()).toEqual [[[0, 2], [1, 9]], [[2, 2], [2, 9]], [[13, 0], [13, 0]]]

     describe ".moveLineDown", ->
       describe "when there is only one line selected", ->


add test for ".5"

add test for ".5"

--- spec/time_parser_spec.coffee
+++ spec/time_parser_spec.coffee
@@ -30,6 +30,8 @@
         expect(@parser.to_minutes("12")).toEqual 12

     describe 'with decimals', ->
+      it 'should turn .5 into half an hour', ->
+        expect(@parser.to_minutes(".5")).toEqual 30
       it 'should turn 0.5 into half an hour', ->
         expect(@parser.to_minutes("0.5")).toEqual 30


Take 4

Take 4

--- src/adapters/campfire.coffee
+++ src/adapters/campfire.coffee
@@ -41,6 +41,7 @@
     bot.on "TextMessage", withAuthor (id, created, room, user, body, author) ->
       unless bot.info.id == author.id
         self.receive new Robot.TextMessage(author, body)
+        console.log(author)

     bot.on "EnterMessage", withAuthor (id, created, room, user, body, author) ->
       unless bot.info.id == author.id
@@ -52,7 +53,6 @@

     bot.on "TimestampMessage", withAuthor (id, created, room, user, body, author) ->
       console.log(author)
-      self.receive new Robot.TimestampMessage(null)

     bot.Me (err, data) ->
       bot.info = data.user


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/engine_game_state.coffee
+++ src/engine_game_state.coffee
@@ -51,7 +51,7 @@
       else
         I.currentState.cameras()
     fadeIn: (options={}) ->
-
+      self.cameras.invoke('fadeOut', options)
     flash: (options={}) ->
       self.camera(options.camera).flash(options)
     objects: ->


Fix handling of linter warnings

Fix handling of linter warnings

--- src/fs_utils/pipeline.coffee
+++ src/fs_utils/pipeline.coffee
@@ -74,6 +74,7 @@
         logger.warn "Linting of #{path}: #{error}"
       else
         return callback throwError 'Linting', error if error?
-        compile source, path, compilers, callback
+
+      compile source, path, compilers, callback

 exports.pipeline = pipeline


client: fix reference error

client: fix reference error

--- client/home/lib/billing/components/paymentsectioncontainer.coffee
+++ client/home/lib/billing/components/paymentsectioncontainer.coffee
@@ -2,6 +2,7 @@
 { connect } = require 'react-redux'
 { createSelector } = require 'reselect'

+getGroup = require 'app/util/getGroup'
 getGroupStatus = require 'app/util/getGroupStatus'

 { CREATE_TOKEN } = stripe = require 'app/redux/modules/stripe'
@@ -35,7 +36,7 @@

 formMessage = createSelector(
   stripe.lastAction
-  (lastAction) -> formMessages[lastAction or getGroupStatus()]
+  (lastAction) -> formMessages[lastAction or getGroupStatus(getGroup())]
 )

 submitting = (formName) -> (state) -> state.form[formName]?.submitting


home/tryonkoding: remove unneccessary click handler func.

--- client/home/lib/utilities/components/tryonkoding/container.coffee
+++ client/home/lib/utilities/components/tryonkoding/container.coffee
@@ -72,19 +72,12 @@
     .catch ({ message }) -> @setState { checked: not state }

-  handleCodeBlockClick: ->
-
-    copyContent = @refs.view.refs.textarea
-    copyToClipboard copyContent
-
-
   render: ->

     <View
       ref='view'
       value={@state.value}
       checked={@state.checked}
-      handleCodeBlockClick={@bound 'handleCodeBlockClick'}
       primaryClassName={@state.primaryClassName}
       secondaryClassName={@state.secondaryClassName}
       handleSwitch={@bound 'handleSwitch'}/>


Add max operator

Add max operator

--- server/models/indicator.coffee
+++ server/models/indicator.coffee
@@ -64,6 +64,10 @@
     _.filter(data, (row) ->
       row[field] >= value
     )
+  max: (data, field, value) ->
+    _.filter(data, (row) ->
+      row[field] <= value
+    )

 indicatorSchema.methods.calculateIndicatorDataBounds = (callback) ->
   @getIndicatorData((error, data) =>


KDContentEditableView: trigger 'focus' event regardless of the focus state

--- src/components/inputs/contenteditableview.coffee
+++ src/components/inputs/contenteditableview.coffee
@@ -65,7 +65,7 @@

   focus: ->
     @unsetPlaceholder() if @getValue().length is 0
-    @getEditableDomElement().trigger "focus" unless @focused
+    @getEditableDomElement().trigger "focus"

     windowController = KD.getSingleton "windowController"
     windowController.addLayer this


Fix finding the next task key

Fix finding the next task key

--- app/classifier/index.cjsx
+++ app/classifier/index.cjsx
@@ -89,11 +89,12 @@

     if TaskComponent is tasks.single
       currentAnswer = task.answers?[annotation.value]
-    # If the next task key exists, make sure the task it points to actually exists.
-    nextTaskKey = if currentAnswer? and @props.workflow.tasks[currentAnswer.next]?
-      currentAnswer.next
-    else if @props.workflow.tasks[task.next]?
-      task.next
+      nextTaskKey = currentAnswer?.next
+    else
+      nextTaskKey = task.next
+
+    unless @props.workflow.tasks[nextTaskKey]?
+      nextTaskKey = ''

     # TODO: Actually disable things that should be.
     disabledStyle =


Remove comment.

Remove comment.

--- app/client/js/main.coffee
+++ app/client/js/main.coffee
@@ -13,11 +13,4 @@
     with: element: document.body
   container.resolveApp()

-  # TODO(steida): Move endpoint constant to server data.
-  # myFirebaseRef = new Firebase 'https://shining-fire-6810.firebaseio.com/'
-  # myFirebaseRef
-  #   .child 'songs'
-  #   .on 'value', (snapshot) ->
-  #     console.log snapshot.val()
-
 goog.exportSymbol 'app.main', app.main


Cast ID to string when removing a command

--- extension/options.coffee
+++ extension/options.coffee
@@ -110,7 +110,7 @@
     @$importList.append commands

   onClickCommandRemove: (e) =>
-    id = $(e.target).data("id")
+    id = "#{$(e.target).data("id")}"
     command = _.findWhere @getCustomCommands(), gistID: id
     if confirm "Are you sure you want to remove \"#{command.name}\""
       @removeCommand id


social: faster jmachine fetch query

social: faster jmachine fetch query

--- workers/social/lib/social/models/computeproviders/machine.coffee
+++ workers/social/lib/social/models/computeproviders/machine.coffee
@@ -230,9 +230,8 @@

       { r: { group, user } } = client

-      selector       ?= { }
-      selector.users  = $elemMatch: id: user.getId()
-      selector.groups = $elemMatch: id: group.getId()
+      selector             ?= {}
+      selector['users.id']  = user.getId()

       JMachine.some selector, limit: 30, (err, machines)->
         callback err, machines


shading plan

shading plan

--- src/javascript/modules/block_maze/objects.coffee
+++ src/javascript/modules/block_maze/objects.coffee
@@ -227,6 +227,7 @@
     # geometry.colorsNeedUpdate = false
     # geometry.uvsNeedUpdate = false
     # geometry.groupsNeedUpdate = false
+    geometry.computeVertexNormals()

       # geometry.vertices[i].z = 2
     # for y in [0...planeHSegs]
@@ -237,7 +238,7 @@

     material = new THREE.MeshPhongMaterial(
       color: physical.data.color
-      wireframe: true
+      # wireframe: true
     )
     mesh = new THREE.Mesh(geometry, material)
     # mesh.castShadow = true


function to convert vertices to polygon object

--- coffeescript/love.js/math.coffee
+++ coffeescript/love.js/math.coffee
@@ -23,18 +23,7 @@
     @random_generator.getSeed(@random_generator)

   isConvex: (vertices...) =>
-    if vertices.length == 1
-      vertices = if vertices[0].__shine
-          # make up for lua being one-indexed with the slice
-          vertices[0].__shine.numValues.slice(1, vertices[0].__shine.numValues.length)
-        else
-          vertices[0]
-
-    polygon = []
-    for i in [0...vertices.length] by 2
-      polygon.push
-        x: vertices[i]
-        y: vertices[i + 1]
+    polygon = toPolygon(vertices)

     i = polygon.length - 2
     j = polygon.length - 1
@@ -112,3 +101,15 @@
           colors[0]
     else
       colors
+
+  toPolygon = (vertices) ->
+    if vertices.length == 1
+      vertices = if vertices[0].__shine
+          # make up for lua being one-indexed with the slice
+          vertices[0].__shine.numValues.slice(1, vertices[0].__shine.numValues.length)
+        else
+          vertices[0]
+
+    for i in [0...vertices.length] by 2
+      x: vertices[i]
+      y: vertices[i + 1]


better help

better help

--- src/steroids/generators/resources/Default.coffee
+++ src/steroids/generators/resources/Default.coffee
@@ -13,10 +13,10 @@

   @usage: ()->
     """
-    Generates a stub resource consisting of a controller and a single index view.
+    Generates a stub resource consisting of a controller and index show views.

     Options:
-      - resource: name of resource to use. example: car will result in a carController and views/car/index.html
+      - resource: name of resource to use. example: car will result in a app/controllers/car.js and views/car/index.html & show.html
     """

   templatePath: ->


typos in tests

typos in tests

--- test/dummy/spec/javascripts/spec/files.js.coffee
+++ test/dummy/spec/javascripts/spec/files.js.coffee
@@ -1,6 +1,6 @@
 describe "UploadedFile", ->

-  it "should be successfuly created with corrent file info", ->
+  it "should be successfully created with correct file info", ->

     mocks.use 'jsonp'
     mocks.jsonp.addHandler /\/info\/$/, (url, data) ->


Fix colorForState for repos list

Fix colorForState for repos list

--- app/views/repos-list.coffee
+++ app/views/repos-list.coffee
@@ -1,5 +1,5 @@
 `import Ember from 'ember'`
-`import colorForState from 'travis/utils/helpers'`
+`import { colorForState } from 'travis/utils/helpers'`

 View = Ember.CollectionView.extend
   elementId: 'repos'
@@ -17,8 +17,7 @@
     ).property('controller.selectedRepo')

     color: (->
-      # TODO: temp comment
-      #colorForState(@get('repo.lastBuildState'))
+      colorForState(@get('repo.lastBuildState'))
     ).property('repo.lastBuildState')

     click: ->


safe why not

safe why not

--- apps/articles/routes.coffee
+++ apps/articles/routes.coffee
@@ -21,7 +21,7 @@
   ]).fail(next).then =>
     res.locals.sd.ARTICLES = articles.toJSON()
     res.locals.sd.ARTICLES_COUNT = articles.count
-    vertical = verticals.running()[0]
+    vertical = verticals.running()?[0]
     res.render 'articles', vertical: vertical, articles: articles

 @show = (req, res, next) ->


Update da.coffee (#5780)

Update da.coffee (#5780)

Co-authored-by: Valentin Bryukhanov <53545457af642b0f8712bb2cc004f120ce4436f4@gmail.com>


Update hu.coffee (#5826)

Update hu.coffee (#5826)


Translate passages on Contribute and About page

--- app/locale/zh-HANS.coffee
+++ app/locale/zh-HANS.coffee
@@ -271,16 +271,16 @@
     who_description_prefix: "在2013年开始一起编写 CodeCombat。在2008年时，我们还创造"
     who_description_suffix: "并且发展出了首选的学习如何写中文和日文的Web和IOS应用"
     who_description_ending: "现在是时候教人们如何写代码了。"
-#    why_paragraph_1: "When making Skritter, George didn't know how to program and was constantly frustrated by his inability to implement his ideas. Afterwards, he tried learning, but the lessons were too slow. His housemate, wanting to reskill and stop teaching, tried Codecademy, but \"got bored.\" Each week another friend started Codecademy, then dropped off. We realized it was the same problem we'd solved with Skritter: people learning a skill via slow, intensive lessons when what they need is fast, extensive practice. We know how to fix that."
-#    why_paragraph_2: "Need to learn to code? You don't need lessons. You need to write a lot of code and have a great time doing it."
-#    why_paragraph_3_prefix: "That's what programming is about. It's gotta be fun. Not fun like"
-#    why_paragraph_3_italic: "yay a badge"
-#    why_paragraph_3_center: "but fun like"
-#    why_paragraph_3_italic_caps: "NO MOM I HAVE TO FINISH THE LEVEL!"
-#    why_paragraph_3_suffix: "That's why CodeCombat is a multiplayer game, not a gamified lesson course. We won't stop until you can't stop--but this time, that's a good thing."
-#    why_paragraph_4: "If you're going to get addicted to some game, get addicted to this one and become one of the wizards of the tech age."
-#    why_ending: "And hey, it's free. "
-#    why_ending_url: "Start wizarding now!"
+    why_paragraph_1: "当我们制作 Skritter 的时候，George 不会写程序，对于不能实现他的灵感这一点很苦恼。他试着学了学，但是那些课程都太慢了。他的室友想不再教书学习新技能，试了试 CodeAcademy，但是觉得“太无聊了。”每个星期都会有个熟人尝试 CodeAcademy，然后无一例外地放弃掉。我们发现这和 Skritter 想要解决的是一个问题：人们想要的是高速学习、充分练习，得到的却是缓慢、冗长的课程。我们知道该怎么办了。"
+    why_paragraph_2: "你想学编程？你不用上课。你需要的是写好多代码，并且享受这个过程。"
+    why_paragraph_3_prefix: "这才是编程的要义。编程必须要好玩。不是"
+    why_paragraph_3_italic: "哇又一个奖章诶"
+    why_paragraph_3_center: "那种“好玩”，而是"
+    why_paragraph_3_italic_caps: "不老妈，我德先把这关打完！"
+    why_paragraph_3_suffix: "这就是为什么 CodeCombat 是个多人游戏，而不是一个游戏化的编程课。你不停，我们就不停——但这次这是件好事。"
+    why_paragraph_4: "如果你一定要对游戏上瘾，那就对这个游戏上瘾，然后成为科技时代的法师吧。"
+    why_ending: "再说，这游戏还是免费的。"
+    why_ending_url: "开始学习法术吧！"
 #    george_description: "CEO, business guy, web designer, game designer, and champion of beginning programmers everywhere."
 #    scott_description: "Programmer extraordinaire, software architect, kitchen wizard, and master of finances. Scott is the reasonable one."
 #    nick_description: "Programming wizard, eccentric motivation mage, and upside-down experimenter. Nick can do anything and chooses to build CodeCombat."
@@ -350,18 +350,18 @@

   contribute:
     page_title: "贡献"
-#    character_classes_title: "Character Classes"
-#    introduction_desc_intro: "We have high hopes for CodeCombat."
-#    introduction_desc_pref: "We want to be where programmers of all stripes come to learn and play together, introduce others to the wonderful world of coding, and reflect the best parts of the community. We can't and don't want to do that alone; what makes projects like GitHub, Stack Overflow and Linux great are the people who use them and build on them. To that end, "
-#    introduction_desc_github_url: "CodeCombat is totally open source"
-#    introduction_desc_suf: ", and we aim to provide as many ways as possible for you to take part and make this project as much yours as ours."
-#    introduction_desc_ending: "We hope you'll join our party!"
+    character_classes_title: "贡献者职业"
+    introduction_desc_intro: "我们对 CodeCombat 有很高的期望。"
+    introduction_desc_pref: "我们希望所有的程序员一起来学习和游戏，让其他人也见识到代码的美妙，并且展现出社区的最好一面。我们不能也不想独自完成这个目标：让 GitHub、Stack Overflow 和 Linux 真正伟大的是它们的用户。为了完成这个目标，"
+    introduction_desc_github_url: "我们把 CodeCombat 完全开源"
+    introduction_desc_suf: "，而且我们希望提供尽可能多的方法让你来参加这个项目，与我们一起创造。"
+    introduction_desc_ending: "我们希望你也会加入进来！"
 #    introduction_desc_signature: "- Nick, George, Scott, Michael, and Jeremy"
-#    alert_account_message_intro: "Hey there!"
-#    alert_account_message_pref: "To subscribe for class emails, you'll need to "
-#    alert_account_message_suf: "first."
-#    alert_account_message_create_url: "create an account"
-#    archmage_summary: "Interested in working on game graphics, user interface design, database and server organization, multiplayer networking, physics, sound, or game engine performance? Want to help build a game to help other people learn what you are good at? We have a lot to do and if you are an experienced programmer and want to develop for CodeCombat, this class is for you. We would love your help building the best programming game ever."
+    alert_account_message_intro: "你好！"
+    alert_account_message_pref: "要订阅贡献者邮件，你得先"
+    alert_account_message_suf: "。"
+    alert_account_message_create_url: "创建账号"
+    archmage_summary: "你对游戏图像、界面设计、数据库和服务器运营、多人在线、物理、声音、游戏引擎性能感兴趣吗？想做一个教别人编程的游戏吗？如果你有编程经验，想要开发 CodeCombat ，那就选择这个职业吧。我们会非常高兴在制作史上最好的编程游戏的过程中得到你的帮助。"
 #    archmage_introduction: "One of the best parts about building games is they synthesize so many different things. Graphics, sound, real-time networking, social networking, and of course many of the more common aspects of programming, from low-level database management, and server administration to user facing design and interface building. There's a lot to do, and if you're an experienced programmer with a hankering to really dive into the nitty-gritty of CodeCombat, this class might be for you. We would love to have your help building the best programming game ever."
 #    class_attributes: "Class Attributes"
 #    archmage_attribute_1_pref: "Knowledge in "
@@ -374,10 +374,10 @@
 #    join_desc_4: "and we'll go from there!"
 #    join_url_email: "Email us"
 #    join_url_hipchat: "public HipChat room"
-#    more_about_archmage: "Learn More About Becoming an Archmage"
+    more_about_archmage: "了解成为大法师的方法"
 #    archmage_subscribe_desc: "Get emails on new coding opportunities and announcements."
-#    artisan_summary_pref: "Want to design levels and expand CodeCombat's arsenal? People are playing through our content at a pace faster than we can build! Right now, our level editor is barebone, so be wary. Making levels will be a little challenging and buggy. If you have visions of campaigns spanning for-loops to"
-#    artisan_summary_suf: "then this class is for you."
+    artisan_summary_pref: "想要设计 CodeCombat 的关卡吗？人们玩的比我们做的快多了！现在我们的关卡编辑器还很基本，所以做起关卡来会有点麻烦，还会有bug。只要你有制作关卡的灵感，不管是简单的for循环还是"
+    artisan_summary_suf: "这种东西，这个职业都很适合你。"
 #    artisan_introduction_pref: "We must construct additional levels! People be clamoring for more content, and we can only build so many ourselves. Right now your workstation is level one; our level editor is barely usable even by its creators, so be wary. If you have visions of campaigns spanning for-loops to"
 #    artisan_introduction_suf: "then this class might be for you."
 #    artisan_attribute_1: "Any experience in building content like this would be nice, such as using Blizzard's level editors. But not required!"
@@ -388,52 +388,52 @@
 #    artisan_join_step2: "Create a new level and explore existing levels."
 #    artisan_join_step3: "Find us in our public HipChat room for help."
 #    artisan_join_step4: "Post your levels on the forum for feedback."
-#    more_about_artisan: "Learn More About Becoming an Artisan"
+    more_about_artisan: "了解成为工匠的方法"
 #    artisan_subscribe_desc: "Get emails on level editor updates and announcements."
-#    adventurer_sumamry: "Let us be clear about your role: you are the tank. You are going to take heavy damage. We need people to try out brand-new levels and help identify how to make things better. The pain will be enormous; making good games is a long process and no one gets it right the first time. If you can endure and have a high constitution score, then this class is for you."
+    adventurer_sumamry: "丑话说在前面，你就是那个挡枪子的，而且你会伤得很重。我们需要人手来测试崭新的关卡，并且提出改进意见。做一个好游戏是一个漫长的过程，没人第一次就能搞对。如果你能忍得了这些，而且身体健壮，那这个职业就是你的了。"
 #    adventurer_introduction: "Let's be clear about your role: you are the tank. You're going to take heavy damage. We need people to try out brand-new levels and help identify how to make things better. The pain will be enormous; making good games is a long process and no one gets it right the first time. If you can endure and have a high constitution score, then this class might be for you."
 #    adventurer_attribute_1: "A thirst for learning. You want to learn how to code and we want to teach you how to code. You'll probably be doing most of the teaching in this case, though."
 #    adventurer_attribute_2: "Charismatic. Be gentle but articulate about what needs improving, and offer suggestions on how to improve."
 #    adventurer_join_pref: "Either get together with (or recruit!) an Artisan and work with them, or check the box below to receive emails when there are new levels to test. We'll also be posting about levels to review on our networks like"
 #    adventurer_forum_url: "our forum"
 #    adventurer_join_suf: "so if you prefer to be notified those ways, sign up there!"
-#    more_about_adventurer: "Learn More About Becoming an Adventurer"
+    more_about_adventurer: "了解成为冒险家的方法"
 #    adventurer_subscribe_desc: "Get emails when there are new levels to test."
-#    scribe_summary_pref: "CodeCombat is not just going to be a bunch of levels. It will also be a resource of programming knowledge that players can hook into. That way, each Artisan can link to a detailed article that for the player's edification: documentation akin to what the "
-#    scribe_summary_sufx: " has built. If you enjoy explaining programming concepts, then this class is for you."
+    scribe_summary_pref: "CodeCombat 不只是一堆关卡的集合，它还是玩家们编程知识的来源。这样的话，每个工匠都能链接详尽的文档，以供玩家们学习，类似于"
+    scribe_summary_sufx: "那些。如果你喜欢解释编程概念，那这个职业适合你。"
 #    scribe_introduction_pref: "CodeCombat isn't just going to be a bunch of levels. It will also include a resource for knowledge, a wiki of programming concepts that levels can hook into. That way rather than each Artisan having to describe in detail what a comparison operator is, they can simply link their level to the Article describing them that is already written for the player's edification. Something along the lines of what the "
 #    scribe_introduction_url_mozilla: "Mozilla Developer Network"
 #    scribe_introduction_suf: " has built. If your idea of fun is articulating the concepts of programming in Markdown form, then this class might be for you."
 #    scribe_attribute_1: "Skill in words is pretty much all you need. Not only grammar and spelling, but able to convey complicated ideas to others."
 #    contact_us_url: "Contact us"
 #    scribe_join_description: "tell us a little about yourself, your experience with programming and what sort of things you'd like to write about. We'll go from there!"
-#    more_about_scribe: "Learn More About Becoming a Scribe"
+    more_about_scribe: "了解成为文书的方法"
 #    scribe_subscribe_desc: "Get emails about article writing announcements."
-    diplomat_summary: "在其他国家不讲英语，很多人对于CodeCombat有很大的兴趣。我们正在寻找愿意花时间翻译网站语料库的词语的译者，这样 CodeCombat 就能尽快地遍及世界各地。如果你想帮助 CodeCombat 的国际化，那么这个类就是给你的。"
-    diplomat_introduction_pref: "如果有一件事情是从 "
-    diplomat_launch_url: "launch in October"
-    diplomat_introduction_suf: "学来的，Combat有相当大的兴趣在其他国家发展。我们正在构建一个译者兵团把单词一个一个的翻译，让CodeCombat尽可能地访问到世界各地。如果你喜欢偷偷地瞄一眼即将到来的内容,并让你的国民尽快学习到CodeCombat,那么这个类可能适合你。"
-    diplomat_attribute_1: "会流利的英语和能翻译的语言。当传递复杂思想，难得的是能很好的同时掌握这两个。"
-    diplomat_join_pref_github: "找到你自己的语言文件 "
-    diplomat_github_url: "在GitHub网站"
-    diplomat_join_suf_github: "在线编辑它，然后提交一个合并请求。同时，选中下面这个复选框来关注最新的国际化开发！"
-    more_about_diplomat: "了解更多“如何成为一名外交官（翻译者）”"
+    diplomat_summary: "很多国家不说英文，但是人们对 CodeCombat 兴致很高！我们需要具有热情的翻译者，来把这个网站上的文字尽快带向全世界。如果你想帮我们走向全球，那这个职业适合你。"
+    diplomat_introduction_pref: "如果说我们从"
+    diplomat_launch_url: "十月的发布"
+    diplomat_introduction_suf: "中得到了什么启发：那就是全球的人对 CodeCombat 都很感兴趣。我们召集了一群翻译者，尽快地把网站上的信息翻译成各国文字。如果你对即将发布的新内容感兴趣，想让你的国家的人们玩上，就快来成为外交官吧。"
+    diplomat_attribute_1: "既会说流利的英语，也熟悉自己的语言。编程是一件很复杂的事情，而要翻译复杂的概念，你必须对两种语言都在行！"
+    diplomat_join_pref_github: "在"
+    diplomat_github_url: "GitHub"
+    diplomat_join_suf_github: "找到你的语言文件，在线编辑它，然后提交一个合并请求。同时，选中下面这个复选框来关注最新的国际化开发！"
+    more_about_diplomat: "了解成为外交官的方法"
     diplomat_subscribe_desc: "接受有关国际化开发和翻译情况的邮件"
-#    ambassador_summary: "We are trying to build a community, and every community needs a support team when there are troubles. We have got chats, emails, and social networks so that our users can get acquainted with the game. If you want to help people get involved, have fun, and learn some programming, then this class is for you."
+    ambassador_summary: "我们要建立一个社区，而当社区遇到麻烦的时候，就要支持人员出场了。我们运用 IRC、电邮、社交网站等多种平台帮助玩家熟悉游戏。如果你想帮人们参与进来，学习编程，然后玩的开心，那这个职业属于你。"
 #    ambassador_introduction: "This is a community we're building, and you are the connections. We've got Olark chats, emails, and social networks with lots of people to talk with and help get acquainted with the game and learn from. If you want to help people get involved and have fun, and get a good feel of the pulse of CodeCombat and where we're going, then this class might be for you."
 #    ambassador_attribute_1: "Communication skills. Be able to identify the problems players are having and help them solve them. Also, keep the rest of us informed about what players are saying, what they like and don't like and want more of!"
 #    ambassador_join_desc: "tell us a little about yourself, what you've done and what you'd be interested in doing. We'll go from there!"
 #    ambassador_join_note_strong: "Note"
 #    ambassador_join_note_desc: "One of our top priorities is to build multiplayer where players having difficulty solving levels can summon higher level wizards to help them. This will be a great way for ambassadors to do their thing. We'll keep you posted!"
-#    more_about_ambassador: "Learn More About Becoming an Ambassador"
+    more_about_ambassador: "了解成为使节的方法"
 #    ambassador_subscribe_desc: "Get emails on support updates and multiplayer developments."
-#    counselor_summary: "None of the above roles fit what you are interested in? Do not worry, we are on the lookout for anybody who wants a hand in the development of CodeCombat! If you are interested in teaching, game development, open source management, or anything else that you think will be relevant to us, then this class is for you."
+    counselor_summary: "以上的职业都不适合你？没关系，我们欢迎每一个想参与 CodeCombat 开发的人！如果你熟悉教学、游戏开发、开源管理，或者任何你觉得和我们有关的方面，那这个职业属于你。"
 #    counselor_introduction_1: "Do you have life experience? A different perspective on things that can help us decide how to shape CodeCombat? Of all these roles, this will probably take the least time, but individually you may make the most difference. We're on the lookout for wisened sages, particularly in areas like: teaching, game development, open source project management, technical recruiting, entrepreneurship, or design."
 #    counselor_introduction_2: "Or really anything that is relevant to the development of CodeCombat. If you have knowledge and want to share it to help grow this project, then this class might be for you."
 #    counselor_attribute_1: "Experience, in any of the areas above or something you think might be helpful."
 #    counselor_attribute_2: "A little bit of free time!"
 #    counselor_join_desc: "tell us a little about yourself, what you've done and what you'd be interested in doing. We'll put you in our contact list and be in touch when we could use advice (not too often)."
-#    more_about_counselor: "Learn More About Becoming a Counselor"
+    more_about_counselor: "了解成为顾问的方式"
     changes_auto_save: "在您切换复选框时，更改将自动保存。"
     diligent_scribes: "我们勤奋的文书:"
     powerful_archmages: "我们强力的大法师:"


teacher_dashborad i18n

teacher_dashborad i18n

--- app/locale/zh-HANS.coffee
+++ app/locale/zh-HANS.coffee
@@ -3182,73 +3182,73 @@
     skip_video: "跳过视频"
     unavailable: "无法获取资源"

-#  teacher_dashboard:
-#    lock: "Lock"
-#    unlock: "Unlock"
-#    teacher_locked_message: "Your teacher has locked the next level"
-#    plan_your_class_title: "Plan your first class by"
-#    plan_your_class1: "Viewing course progression info"
-#    plan_your_class2: "Previewing lesson slides & extension activities"
-#    plan_your_class3: "Trying the first chapter for free"
-#    click_dismiss: "Click to Dismiss"
-#    track_progress: "Track Progress"
-#    track_progress_desc: "Click into the class you just created to view detailed student progress."
-#    add_classes_title: "Add Classes Here"
-#    add_classes: "Add classes and get students started with the first free chapter!"
-#    all_classes: "All Classes"
-#    my_licenses: "My Licenses"
-#    resource_hub: "Resource Hub"
-#    curriculum_guide: "Curriculum Guide"
-#    add_class: "Add New Class"
-#    no_licenses_yet: "No licenses yet"
-#    req_licenses: "Request Licenses"
-#    license_ratio_used: "__totalUsedLicenses__ out of __totalSpots__"
-#    licenses_applied: "Licenses Applied"
-#    student_projects: "Student Projects"
-#    no_classes_yet: "No classes yet"
-#    exemplar_project: "Exemplar Project"
-#    capstone_proj_for: "Capstone Project for __courseName__"
-#    project_rubric: "Project Rubric"
-#    select_chapter: "Select Chapter"
-#    concept_flag: "Concept Flag"
-#    support_learning: "Support Learning"
-#    concept_flag_desc: "More than 50% of the class attempted a Concept Check 3 or more times."
-#    concept_flag_desc2: "Student attempted a Concept Check 3 or more times."
-#    click_progress_dot_tip: "Tip: Click on the progress bubbles for more details"
-#    color_code: "Color Code"
-#    content_guide: "Content Guide"
-#    story: "Story"
-#    cutscenes: "Cutscenes"
-#    cutscenes_tooltip: "
Cutscene

Short videos that explain parts of the epic Ozaria narrative.
"
-#    cinematics: "Cinematics"
-#    concept_checks: "Concept Checks"
-#    practice_levels: "Practice Levels"
-#    challenge_levels: "Challenge Levels"
-#    capstone_levels: "Capstone Levels"
-#    intro: "Intro"
-#    practice: "Practice"
-#    assess: "Assess"
-#    view_options: "View Options"
-#    manage_class: "Manage Class"
-#    assign_content: "Assign Content"
-#    remove_students: "Remove Students"
-#    revoke_licenses: "Revoke Licenses"
-#    remaining_licenses: "__remaining__ license(s) remaining"
-#    start_date: "Start: __date__"
-#    end_date: "End: __date__"
-#    license_questions: "Have questions about applying or revoking licenses?"
-#    need_more_licenses: "Need more licenses? We'll help you build a solution that meets your needs."
-#    support_oz: "support@ozaria.com"
-#    standards_alignment: "Standards Alignment"
-#    standards_sample: "A sample of the standards covered in this Chapter:"
-#    play_chapter: "Play Chapter"
-#    class_time_range: "Class Time (Range):"
-#    in_game_play_time: "In-Game Play Time:"
-#    class_time: "Class Time"
-#    solution_guide: "Solution Guide"
-#    module: "Module"
-#    need_licenses_tooltip: "
You need licenses to access this content!

Please visit the My Licenses page for more information.
"
-#    lesson_slides_tooltip: "
Lesson Slides

Downloadable, step-by-step presentation slides for guiding students through module learning objectives
"
-#    project_rubric_tooltip: "
Project Rubric

Downloadable and modifiable scoring rubric for the Capstone Project
"
-#    exemplar_projects_tooltip: "
Exemplar Project

Live view of the exemplar Capstone Project
"
-#    want_to_save_tooltip: "
Want to save your progress?

Please create a student account and play Ozaria as a student if you would like to save your progress.
"
+  teacher_dashboard:
+    lock: "锁定"
+    unlock: "解锁"
+    teacher_locked_message: "老师锁定了下一个关卡"
+    plan_your_class_title: "计划您的课程"
+    plan_your_class1: "查看课程进度"
+    plan_your_class2: "查看课件和扩展活动"
+    plan_your_class3: "免费试用第一章节"
+    click_dismiss: "点击解散"
+    track_progress: "跟踪进度"
+    track_progress_desc: "点击你创建的班级，查看学生的详细进度"
+    add_classes_title: "这里添加班级"
+    add_classes: "添加班级，学生可以免费试用第一章"
+    all_classes: "所有班级"
+    my_licenses: "我的许可"
+    resource_hub: "资源中心"
+    curriculum_guide: "教学指导"
+    add_class: "添加新班级"
+    no_licenses_yet: "没有许可"
+    req_licenses: "获取许可"
+    license_ratio_used: "全部已使用许可/全部许可"
+    licenses_applied: "已使用许可"
+    student_projects: "学生项目"
+    no_classes_yet: "班级不存在"
+    exemplar_project: "示例项目"
+    capstone_proj_for: "顶点项目"
+    project_rubric: "项目评分标准"
+    select_chapter: "已选章节"
+    concept_flag: "概念标记"
+    support_learning: "学习支持"
+    concept_flag_desc: "超过50%的学生尝试概念测试3次及以上"
+    concept_flag_desc2: "学生尝试概念测试3次及以上"
+    click_progress_dot_tip: "提示：点击进度标识了解更多信息  "
+    color_code: "颜色标记"
+    content_guide: "内容指导"
+    story: "故事"
+    cutscenes: "剧情"
+    cutscenes_tooltip: "剧情短视频描述了奥佳睿史诗故事。"
+    cinematics: "剧情视频"
+    concept_checks: "概念测试"
+    practice_levels: "联系关卡"
+    challenge_levels: "挑战关卡"
+    capstone_levels: "顶点项目"
+    intro: "介绍"
+    practice: "练习"
+    assess: "评估"
+    view_options: "查看选项"
+    manage_class: "管理班级"
+    assign_content: "分配课程"
+    remove_students: "移除学生"
+    revoke_licenses: "收回许可"
+    remaining_licenses: "剩余许可"
+    start_date: "开始时间："
+    end_date: "结束时间："
+    license_questions: "使用和收回许可有疑问吗？ "
+    need_more_licenses: "需要更多学习许可吗? 我们会为您的需求定制方案。 "
+    support_oz: "china@codecombat.com"
+    standards_alignment: "符合标准"
+    standards_sample: "本章涉及的标准样例。"
+    play_chapter: "进入课程"
+    class_time_range: "班级时间区间："
+    in_game_play_time: "学习时间"
+    class_time: "班级时间"
+    solution_guide: "解法说明"
+    module: "模块"
+    need_licenses_tooltip: "
需要学习许可访问课程内容！

请访问 我的许可 页面了解更多信息
"
+    lesson_slides_tooltip: "
教学课件

可下载详细的教学课件用于课堂教学
"
+    project_rubric_tooltip: "
项目评分标准

顶点项目评分标准，可下载根据需要修改
"
+    exemplar_projects_tooltip: "
项目示例

顶点项目示例查看
"
+    want_to_save_tooltip: "
想要保存你的学习进度吗？

请创建学生账号，作为学生学习奥佳睿课程可以保存学习进度.
"


Login: email activation modal copy changed

--- client/Account/AppController.coffee
+++ client/Account/AppController.coffee
@@ -94,20 +94,16 @@
     name or= KD.whoami().profile.firstName
     message =
       """
-      Dear #{name},
+      You need to confirm your email address to continue using Koding and to fully activate your account.

-      Thanks for joining Koding.

+      When you registered, we sent you a link to confirm your email address. Please use that link.

-      You need to confirm your email address to be able to  login Koding and to fully activate your account.
-
-      When you registered, we have sent you a link to confirm your email address, please use that link to be able to continue using Koding.

-
-      If you didn't receive the email or the token is expired, please click to Resend email button below.

+      If you had trouble with the email, please click below to resend it.

       """

     modal = new KDModalView
-      title            : "Please confirm your email address!"
-      width            : 500
+      title            : "#{name}, please confirm your email address!"
+      width            : 600
       overlay          : yes
       cssClass         : "new-kdmodal"
       content          : "
#{Encoder.htmlDecode message}
"


Add an error check

Add an error check

--- spec/path-replacer-spec.coffee
+++ spec/path-replacer-spec.coffee
@@ -40,6 +40,7 @@
       fs.writeFileSync(filePath, sampleContent)

     it "can make a replacement", ->
+      replacer.on('file-error', errorHandler = jasmine.createSpy())
       replacer.on('path-replaced', resultsHandler = jasmine.createSpy())
       replacer.replacePaths(/items/gi, 'omgwow', [filePath], finishedHandler = jasmine.createSpy())

@@ -47,6 +48,7 @@
         finishedHandler.callCount > 0

       runs ->
+        expect(errorHandler).not.toHaveBeenCalled()
         expect(resultsHandler).toHaveBeenCalled()
         expect(resultsHandler.mostRecentCall.args[0]).toEqual
           filePath: filePath


Fixed setDraggable method of KDView.

Fixed setDraggable method of KDView.

--- client/Framework/core/KDView.coffee
+++ client/Framework/core/KDView.coffee
@@ -579,10 +579,10 @@

       @dragIsAllowed = yes

-      top    = parseInt @$()[0].style.top, 10
-      right  = parseInt @$()[0].style.right, 10
-      bottom = parseInt @$()[0].style.bottom, 10
-      left   = parseInt @$()[0].style.left, 10
+      top    = parseInt (@$()[0].style.top or 0), 10
+      right  = parseInt (@$()[0].style.right or 0), 10
+      bottom = parseInt (@$()[0].style.bottom or 0), 10
+      left   = parseInt (@$()[0].style.left or 0), 10

       @dragState.startX     = event.pageX
       @dragState.startY     = event.pageY


KDView: Added threshold for starting to drag.

--- client/Framework/core/KDView.coffee
+++ client/Framework/core/KDView.coffee
@@ -682,6 +682,8 @@
     @setEmptyDragState()
     handle = if options.handle instanceof KDView then options.handle else @

+    @on "DragFinished", (e) => @isDragging = no
+
     handle.on "mousedown", (event)=>
       if "string" is typeof options.handle
         return if $(event.target).closest(options.handle).length is 0
@@ -725,6 +727,14 @@
     dragDir      = @dragState.direction
     dragGlobDir  = dragDir.global
     dragCurDir   = dragDir.current
+
+    draggedDistance  = Math.max Math.abs(x), Math.abs(y)
+    if draggedDistance < 30 and not @isDragging
+      @dragIsAllowed = no
+      @isDragging    = no
+    else
+      @dragIsAllowed = yes
+      @isDragging    = yes

     if x > dragRelPos.x
       dragCurDir.x  = 'right'


appctrl: kd/keyboard is obsolete

appctrl: kd/keyboard is obsolete

--- client/app/lib/appcontroller.coffee
+++ client/app/lib/appcontroller.coffee
@@ -1,7 +1,5 @@
 kd                 = require 'kd'
 KDViewController   = kd.ViewController
-KDKeyboardListener = kd.KeyboardListener
-KDKeyboardMap      = kd.KeyboardMap
 getAppOptions      = require './util/getAppOptions'

@@ -20,28 +18,6 @@
       kd.utils.defer  =>
         { appStorageController } = kd.singletons
         @appStorage = appStorageController.storage name, version or "1.0.1"
-
-    @bindKeyCombos()
-
-
-  bindKeyCombos: ->
-
-    @appKeyListener = new KDKeyboardListener
-    @appKeyMap      = new KDKeyboardMap { priority: 10 }
-
-    @appKeyListener
-      .addComboMap @appKeyMap
-
-    @registerDeclaredBindings()
-
-    kd.singletons.appManager.on 'AppIsBeingShown', (app) =>
-      @appKeyListener?.listen()  if app is this
-
-
-  registerAppKeys: (bindings = {}) ->
-    @appKeyMap.addCombo binding, fn  for own binding, fn of bindings
-    @appKeyListener.addComboMap @appKeyMap
-    @appKeyMap

   createContentDisplay:(models, callback)->
@@ -63,12 +39,3 @@
       @[cmd]?.call this, event
     else
       throw new Error "Unknown command: #{command}"
-
-
-  registerDeclaredBindings: ->
-    appName = @getOptions().name
-    { keyBindings } = getAppOptions appName
-
-    keyBindings?.forEach (b) =>
-      @appKeyMap.addCombo b.binding, { global: b.global }, (ev) =>
-        @handleCommand b.command, appName, ev


Add spec for clicking installed package from Install panel

--- spec/settings-view-spec.coffee
+++ spec/settings-view-spec.coffee
@@ -151,3 +151,20 @@
         waits 1
         runs ->
           expect(settingsView.activePanelName).toBe 'Install'
+
+  describe "when an installed package is clicked from the Install panel", ->
+    it "displays the package details", ->
+      waitsFor ->
+        atom.packages.activatePackage('settings-view')
+
+      runs ->
+        settingsView.packageManager.getClient()
+        spyOn(settingsView.packageManager.client, 'featuredPackages').andCallFake (callback) ->
+          callback(null, [{name: 'settings-view'}])
+        settingsView.showPanel('Install')
+
+      waitsFor ->
+        settingsView.find('.package-card:not(.hidden)').length > 0
+
+      runs ->
+        settingsView.find('.package-card:not(.hidden):first').click()


More merge testing

More merge testing

--- specs/schema/table.spec.coffee
+++ specs/schema/table.spec.coffee
@@ -93,10 +93,20 @@
         rows = h.testData.newData()
         uq = "UQ_Fighters_LastName_FirstName"

+        merge = fighters.classifyRowsForMerging(rows)
+        o = mergeObj()
+        o.mergesByKey[uq] = rows
+
+        merge.should.eql(o)
+
+        rows[0].id = 1
+        rows[1].id = 2
+        rows[1].lastName = null

         merge = fighters.classifyRowsForMerging(rows)
         o = mergeObj()
-        o.mergesByKey[uq] = rows
+        o.updatesByKey.PK_Fighters = [rows[0], rows[1]]
+        o.mergesByKey[uq] = [rows[2], rows[3]]

         merge.should.eql(o)

@@ -105,4 +115,3 @@
         rows[0].lastName = null
         f = () -> fighters.classifyRowsForMerging(rows)
         f.should.throw(/Cannot merge row/)
-


show toolpad when in hyperlink

show toolpad when in hyperlink

--- app/scripts/editor.coffee
+++ app/scripts/editor.coffee
@@ -208,13 +208,13 @@
 			sel = @selection()
 			if sel.rangeCount > 0
 				range = sel.getRangeAt 0
-				if range.collapsed
-					@hideToolpad()
-				else
+				if !range.collapsed || @getSelectedElement().is 'a'
 					@saveSelection 'toolpad'
 					@showToolpadOverSelection 'toolpad'
 					@restoreSelection 'toolpad'
 					self.linkRange = self.selection().getRangeAt 0
+				else
+					@hideToolpad()
 		# apply link URL
 		$('#toolpad').delegate 'li:first-child', 'click', (e)->
 			self.openLinkInput()


make prompt works right

make prompt works right

--- app/scripts/editor.coffee
+++ app/scripts/editor.coffee
@@ -1,5 +1,5 @@
 @Editor = class Editor
-	promptMessage: 'Type your article here'
+	promptMessage: 'Type your article here'
 	status:
 		cmd: false
 		ctrl: false
@@ -7,8 +7,8 @@
 		shift: false
 		empty: false
 	displayPrompt: ->
-		$('#editor').html "#{@promptMessage}
"
-	hidePrompt: ->
+		$('#editor').html @promptMessage
+	clear: ->
 		$('#editor').html '

'
 	selection: ->
 		window.getSelection() if window.getSelection
@@ -47,7 +47,7 @@
 		return if el[0].nodeType == 3 then el.parent() else el
 	clearStatus: ->
 		$('.debug-status').addClass 'hidden'
-		@status.cmd = @status.ctrl = @status.alt = @status.shift = @status.empty = false
+		@status.cmd = @status.ctrl = @status.alt = @status.shift = false
 	rand: (len=4)->
 		result = ''
 		result += Math.random().toString(36).substr(2,1) for i in new Array(len)
@@ -57,6 +57,8 @@
 		$('p,h1,h2,pre').not('[name]').each ->
 			$(@).attr 'name', self.rand()
 		$('#debug').text $('#editor').html()
+	checkEmpty: ->
+		@status.empty = $('#editor').text() == '' || $('#editor').html() == @promptMessage
 	toggleFormatBlock: (tag)->
 		el = @getSelectedElement()
 		if el.attr 'name'
@@ -136,9 +138,11 @@
 		.on 'mouseup', (e)=>
 			@selection()
 		.blur =>
-			@displayPrompt()
+			@checkEmpty()
+			@displayPrompt() if @status.empty
 			@clearStatus()
 		.focus =>
-			@hidePrompt()
+			@checkEmpty()
+			@clear() if @status.empty
 			@update()
 			@clearStatus()


pricing: initially hide plan form views

pricing: initially hide plan form views

--- client/Pricing/AppController.coffee
+++ client/Pricing/AppController.coffee
@@ -13,8 +13,8 @@
     super options, data

     @productForm      = new KDView
-    @resourcePackPlan = new ResourcePackPlan
-    @customPlan       = new CustomPlan
+    @resourcePackPlan = new ResourcePackPlan cssClass: "hidden"
+    @customPlan       = new CustomPlan cssClass: "hidden"

     @productForm.addSubView @resourcePackPlan
     @resourcePackPlan.on "PlanSelected", @bound "selectPlan"


Client: Don't assume a default item for Pokemon.

--- client/app/js/models/pokemon.coffee
+++ client/app/js/models/pokemon.coffee
@@ -1,7 +1,6 @@
 class @Pokemon extends Backbone.Model
   defaults: =>
     name: 'Bulbasaur'
-    item: ""
     moves: []
     pixels: 48
     ivs:


extract generic base class from Media.Scraper; rename to ThumbnailScraper

--- scripts/media.coffee
+++ scripts/media.coffee
@@ -5,72 +5,65 @@
     @scrapers.push scraper

   loadMedia: (message, link) ->
-    container = $('.media', message)
-    uri       = new Uri link.href
+    uri = new Uri link.href

     for scraper in @scrapers
       if scraper.doesUriMatch uri
-        break if new scraper(container, link, uri).scrape()
+        break if new scraper(message, link, uri).scrape()

-class Media.Scraper
-  THROBBER_URI  = 'images/throbber.gif'
-  LOADING_TITLE = 'loading\u2026'
-
+notImplemented = (constructor, method) ->
+  constructor::[method] = -> throw new Error "Method #{method} must be implemented by #{constructor}"
+
+class Media.BasicScraper
   @doesUriMatch: (uri) -> false

-  constructor: (container, link, @uri) ->
-    @container = $(container)
-    @source    = $(link)
+  constructor: (message, link, @uri) ->
+    @message = $(message)
+    @source  = $(link)

     @isCancelled = false

   scrape: ->
     try
-      @container.append @createDefaultThumbnail()
-      @loadThumbnail()
+      @createDefaultPreview()
+      @loadPreview()
     catch ex
+      console.log ex
       @cancel()

     not @isCancelled

   cancel: ->
-    @thumbnail.remove()
+    @preview.remove()
     @isCancelled = true

-  loadThumbnail: ->
-    @loadThumbnailTitle()
-    @loadThumbnailImage()
-    @loadThumbnailLink()
+  notImplemented this, 'createDefaultPreview'
+  notImplemented this, 'loadPreview'
+
+class Media.ThumbnailScraper extends Media.BasicScraper
+  THROBBER_URI  = 'images/throbber.gif'

-  loadThumbnailTitle: ->
-    @setThumbnailTitle @source.text()
-
-  loadThumbnailImage: ->
-    null
-
-  loadThumbnailLink: ->
-    @setThumbnailLink @source[0].href
-
-  setThumbnailTitle: (title) ->
+  setPreviewTitle: (title) ->
     @thumbnailLink.attr title: title
     @thumbnailImage.attr alt: title

-  setThumbnailImage: (uri) ->
+  setPreviewImage: (uri) ->
     @thumbnailImage.attr src: uri

-  setThumbnailLink: (uri) ->
+  setPreviewLink: (uri) ->
     @thumbnailLink.attr href: uri

-  createDefaultThumbnail: ->
+  createDefaultPreview: ->
     @thumbnailImage = $('<img>')
-                      .attr(src: THROBBER_URI, alt: LOADING_TITLE)
+                      .attr(src: THROBBER_URI, alt: @source.text())
     @thumbnailLink  = $('<a>')
-                      .attr(href: @uri, title: LOADING_TITLE)
-    @thumbnail      = $('<div>')
+                      .attr(href: @source[0].href, title: @source.text())
+    @preview        = $('<div>')
                       .addClass('media-item')
                       .append(@thumbnailLink.append @thumbnailImage)
+    @preview.appendTo $('.media', @message)

-class YouTubeScraper extends Media.Scraper
+class YouTubeScraper extends Media.ThumbnailScraper
   Media.register this

   @doesUriMatch: (uri) ->
@@ -80,7 +73,7 @@
     else
       uri.host is 'youtu.be'

-  loadThumbnail: ->
+  loadPreview: ->
     if @uri.host is 'youtu.be'
       id = uri.path.substring 1
     else
@@ -96,11 +89,11 @@
             xml.lookupNamespaceURI ns
         xpath = (query) -> xml.evaluate(query, xml, resolver, XPathResult.STRING_TYPE, null).stringValue

-        @setThumbnailImage xpath '/atom:entry/media:group/media:thumbnail[@yt:name="default"]/@url'
-        @setThumbnailTitle xpath '/atom:entry/atom:title'
-        @setThumbnailLink @uri
+        @setPreviewImage xpath '/atom:entry/media:group/media:thumbnail[@yt:name="default"]/@url'
+        @setPreviewTitle xpath '/atom:entry/atom:title'
+        @setPreviewLink @uri

-class ImgurScraper extends Media.Scraper
+class ImgurScraper extends Media.ThumbnailScraper
   Media.register this

   @doesUriMatch: (uri) ->
@@ -109,16 +102,16 @@
     else
       false

-  loadThumbnail: ->
+  loadPreview: ->
     id = @uri.path.match(/// ([^/]+) (?: \. [^/]+ )? $ ///)?[1]

     if id?
       $.getJSON "http://api.imgur.com/2/image/#{id}", (data) =>
-        @setThumbnailTitle data.image.image.title ? @source.text()
-        @setThumbnailImage data.image.links.small_square
-        @setThumbnailLink data.image.links.imgur_page
+        @setPreviewTitle data.image.image.title ? @source.text()
+        @setPreviewImage data.image.links.small_square
+        @setPreviewLink data.image.links.imgur_page

-class GenericImageScraper extends Media.Scraper
+class GenericImageScraper extends Media.ThumbnailScraper
   Media.register this

   PATTERN = /// \. (?: bmp | gif | jp2 | jpe?g | png | tiff? ) $///i
@@ -126,7 +119,7 @@
   @doesUriMatch: (uri) ->
     uri.path.match(PATTERN)?

-  loadThumbnailImage: ->
-    @setThumbnailImage 'images/camera.png'
+  loadPreview: ->
+    @setPreviewImage 'images/camera.png'

 window.Media = Media;


Improve highlighting of chained commands in RUNOFF

--- grammars/runoff.cson
+++ grammars/runoff.cson
@@ -110,12 +110,22 @@

 			# Generic command
 			name: "meta.function.runoff"
-			begin: "(?i)(?:^|(?<=;))((\\.)(?:END\\s+)?[^;\\s\\-+]+)"
-			end:   "$|(;)(?=\\.)|(?=;[^.])"
+			begin: "(?i)((\\.)(?:END\\s+)?([^;\\s\\-+]+))"
+			end: """(?x)
+				$              | # EOL
+				(;)\\s*(?=\\.) | # Followed by another command
+				(?=;\\s*[^.])    # No more commands, don’t highlight trailing text as arguments
+			"""
 			patterns: [include: "#arguments"]
 			beginCaptures:
 				1: name: "keyword.function.name.runoff"
 				2: name: "punctuation.definition.function.runoff"
+				3: patterns: [
+					# Allow punctuation/parameters in clustered command-chains to be highlighted:
+					# .LM0.RM80 is likely a compressed form of ".LM 0; .RM 80"
+					{match: "\\.", name: "punctuation.delimiter.period.full-stop.runoff"}
+					{match: "\\d+(?=$|\\n|\\.(?!\\d))", name: "constant.numeric.runoff"}
+				]
 			endCaptures:
 				1: name: "punctuation.terminator.statement.runoff"
 		}]


new and improved

new and improved

--- scripts/satan.coffee
+++ scripts/satan.coffee
@@ -1,17 +1,16 @@
 # Display a picture of Satan if anyone invokes "satan" or says "666"

-images = [
-  "http://images1.wikia.nocookie.net/__cb20110922210808/uncyclopedia/images/0/0c/Satandrawing.jpg"
-  "http://img.photobucket.com/albums/v418/bawanaal/satan.jpg"
-  "http://www.borowitzreport.com/wp-content/uploads/satan3.jpg"
-  "http://upload.wikimedia.org/wikipedia/en/e/e7/Satan_Pit.jpg"
-  "http://www.zap2it.com/media/photo/2009-02/45296660.jpg"
-  "http://freakipedia.net/images/1/1d/Satan.jpg"
-  "http://www2.ic.edu/aporter/docs/361/south-park-SatanSad4_couch_revised_big.jpg"
-  "http://diablo3guideonline.files.wordpress.com/2012/04/diablo1.jpg"
-  "http://softsupplier.com/wp-content/plugins/jobber-import-articles/photos/136495-running-diablo-iii-what-hardware-do-you-need.jpg"
-]
+module.exports = (robot) ->
+  robot.hear /(satan|666|devil|lucifer|santa|stan)/i, (msg) ->
+    imageMe msg, msg.match[1], (url) ->
+      msg.send url

-module.exports = (robot) ->
-  robot.hear /(satan|666|devil)/i, (msg) ->
-    msg.send msg.random images
+imageMe = (msg, query, cb) ->
+  msg.http('http://ajax.googleapis.com/ajax/services/search/images')
+    .query(v: "1.0", rsz: '8', q: query)
+    .get() (err, res, body) ->
+      images = JSON.parse(body)
+      images = images.responseData.results
+      if images.length > 0
+        image  = msg.random images
+        cb "#{image.unescapedUrl}#.png"


Improve formatting of core

Improve formatting of core

--- handlers/core.coffee
+++ handlers/core.coffee
@@ -64,11 +64,22 @@
 			else
 				callback?()
 		when 'about'
-			api.replyTo message, __("This is Tim’s Chatbot.\nIt is licensed under the terms of the GNU Affero General Public License. You can obtain a copy of the Chatbot at https://github.com/wbbaddons/Tims-Chatbot."), no, callback
+			api.replyTo message, __("""
+				This is Tim’s Chatbot.
+				It is licensed under the terms of the GNU Affero General Public License. You can obtain a copy of the Chatbot at https://github.com/wbbaddons/Tims-Chatbot.
+			"""), no, callback
 		when 'getStats'
 			db.checkPermissionByMessage message, 'core.getStats', (hasPermission) ->
 				if hasPermission
-					api.replyTo message, __("Statistics:\nUp since: %1$s (%2$d seconds)\nMemory Usage: %3$d} KiB", config.upSince, process.uptime(), process.memoryUsage().rss / 1024), no, callback
+					api.replyTo message, __("""
+						Statistics:
+						Up since: %1$s (%2$d seconds)
+						Memory Usage: %3$d} KiB
+						""",
+						config.upSince,
+						process.uptime(),
+						(process.memoryUsage().rss / 1024)
+					), no, callback
 				else
 					callback?()
 		else


Add missing setImmediate

Add missing setImmediate

--- drivers/javascript/cursor.coffee
+++ drivers/javascript/cursor.coffee
@@ -231,7 +231,7 @@
     addListener: (args...) ->
         if not @emitter?
             @_makeEmitter()
-            @_each @_eachCb
+            setImmediate => @_each @_eachCb
         @emitter.addListener(args...)

     on: (args...) ->


PR #767: Add existential check to fix breaking test

--- src/middleware/router.coffee
+++ src/middleware/router.coffee
@@ -106,7 +106,7 @@
       message: err.message
       stack: err.stack if err.stack

-  logger.error "[#{ctx.transactionId.toString()}] Internal server error occured: #{err}"
+  logger.error "[#{ctx.transactionId?.toString()}] Internal server error occured: #{err}"
   logger.error "#{err.stack}" if err.stack


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/number_extensions.coffee
+++ src/number_extensions.coffee
@@ -73,7 +73,7 @@
 Number::round = ->
   Math.round(this)

-Number::circularPoints = ()
+Number::circularPoints = (n) -

 ###*
 Returns a number whose value is limited to the given range.


home/flux: fix cyclomatic complexity

home/flux: fix cyclomatic complexity

--- client/home/lib/flux/actions.coffee
+++ client/home/lib/flux/actions.coffee
@@ -52,16 +52,8 @@
   if reactor.evaluate(EnvironmentGetters.stacks).size
     markAsDone 'stackCreation'

-
   unobserve = reactor.observe EnvironmentGetters.stacks, (_stacks) ->
-    isStackBuilt = no
-    _stacks.forEach (stack) ->
-      return  if isStackBuilt
-      status = stack.get 'status'
-      if status isnt 'NotInitialized'
-        kd.utils.defer -> markAsDone 'buildStack'
-        isStackBuilt = yes
-        unobserve()
+    checkStacksForBuild _stacks, unobserve

   remote.api.JCredential.some {}, { limit: 1 }, (err, res) ->
     return  if err
@@ -92,7 +84,16 @@
       kd.utils.killRepeat kiteTimer
       markAsDone 'installKd'

+checkStacksForBuild = (stacks, unobserve) ->

+  isStackBuilt = no
+  stacks.forEach (stack) ->
+    return  if isStackBuilt
+    status = stack.get 'status'
+    if status isnt 'NotInitialized'
+      kd.utils.defer -> markAsDone 'buildStack'
+      isStackBuilt = yes
+      unobserve()

 module.exports = {


login: typo

login: typo

--- client/landing/Login/AppView.coffee
+++ client/landing/Login/AppView.coffee
@@ -572,7 +572,7 @@
           @redeemForm.inviteCode.input.setFocus()
         when "login"
           @formHeader.show()
-          @formHeader.updatePartial "Dont't have an account yet? "
+          @formHeader.updatePartial "Don't have an account yet? "
           @formHeader.addSubView @goToRegisterLink
           @loginForm.username.input.setFocus()
         when "recover"


home: fixed pricing copy

home: fixed pricing copy

--- client/landing/introapp/main.coffee
+++ client/landing/introapp/main.coffee
@@ -156,10 +156,10 @@
       slogan     : "Pricing"
       subSlogan  : """

-          When you hire someone, they can get up to speed in your development environment in 5 minutes—easily collaborating with others and contributing code.  All without sharing ssh keys or passwords.  Stop cc'ing your team; stop searching through old emails.
-

-

-          Koding is your new workspace.
+          You'll be able to buy more resources for your personal account or for accounts in your organization.
+

+

+          Coming soon.

       """


made the router scroll to the top of the page on a tag page changes

--- client/scripts/routes/router.coffee
+++ client/scripts/routes/router.coffee
@@ -78,6 +78,9 @@
         # Route using the Backbone router without a page refresh
         @navigate(route, {trigger: true})

+        # Scroll to the top of the new page
+        window.scrollTo(0,0)
+
     # Gets the href attribute from an element, or if null, from the element's first parent that has the attribute
     getHref: (elt) ->
         if elt.hasAttribute("href")


remotely updated content now reloads Aloha

--- scripts/views/workspace/content/aloha-edit.coffee
+++ scripts/views/workspace/content/aloha-edit.coffee
@@ -20,6 +20,7 @@
       @isLoaded = @model.isNew()

       # images only have to be loaded the first time
+      # FIXME: This mess of Deferreds needs to be cleaned up. `XhtmlModel._loadComplex` comes to mind.
       if @model.get(@modelKey)?.length
         @imagesLoaded = (new $.Deferred()).resolve()
       else
@@ -33,8 +34,20 @@
         $(window).unbind 'oer.images.loaded'
         @imagesLoaded.resolve()

-      @listenTo @model, "change:#{@modelKey}", =>
-        @contentLoaded.resolve() if @model.get(@modelKey)?.length
+      @listenTo @model, "change:#{@modelKey}", (model, value, options) =>
+        return if options.internalAlohaUpdate
+
+        if @model.get(@modelKey)?.length
+          # FIXME: SHould **not** depend on the state of the promise.
+          if 'resolved' == @contentLoaded.state()
+            if @model.isDirty()
+              console.log('Discarding local changes because of remote commit')
+            else
+              console.log('Updating local content because of remote changes (but there were no local changes)')
+            @render()
+          else
+            @contentLoaded.resolve()
+

       # if content is already present change will never fire
       # so check that and conditionally finish the content loading as well


Remove logs

Remove logs

--- apps/sitemaps/routes.coffee
+++ apps/sitemaps/routes.coffee
@@ -96,11 +96,9 @@
   res.render('misc', pretty: true)

 @cities = (req, res, next) ->
-  console.log 'cities'
   partnerFeaturedCities = new PartnerFeaturedCities()
   partnerFeaturedCities.fetch
     success: ->
-      console.log 'partnerFeaturedCities', partnerFeaturedCities.models
       res.set 'Content-Type', 'text/xml'
       res.render 'cities', pretty: true, citySlugs: partnerFeaturedCities.pluck('slug')


Add $PROJECT_NAME and $PROJECT_PATH variables resolution for MYPYPATH setting.

--- lib/init.coffee
+++ lib/init.coffee
@@ -158,7 +158,10 @@
       type: 'string'
       default: ''
       description: '''MYPYPATH to use, is a colon-separated list of directories
-
Note: Use a dot to add the directory containing the file being linted
+
Note: Use a dot to add the directory containing the file being linted.
+      The optionals `$PROJECT_PATH` and `$PROJECT_NAME` variables can be used to resolve the path
+      dynamically depending of the current project. For example:
+      `$PROJECT_PATH/stubs`.
       '''
       order: 26

@@ -380,7 +383,12 @@
       options.env["MYPYPATH"] = ''
     ##Prepend user setting defined MYPYPATH to current system env MYPYPATH
     if @mypyPath?
-      options.env["MYPYPATH"] = ':' + @mypyPath + ':' + options.env["MYPYPATH"] + ':'
+      mypyPathResolved = @mypyPath
+      [projectPath, ...] = atom.project.relativizePath(filePath)
+      if projectPath
+        projectName = path.parse(projectPath).base
+        mypyPathResolved = mypyPathResolved.replace(/\$PROJECT_PATH/g, projectPath).replace(/\$PROJECT_NAME/g, projectName)
+      options.env["MYPYPATH"] = ':' + mypyPathResolved + ':' + options.env["MYPYPATH"] + ':'
     ##Add current folder of the file being linted to MYPYPATH
     options.env["MYPYPATH"] = options.env["MYPYPATH"].replace(/:\.:/g, ':' + rootPath + ':')
     ##Clean all repeated path separator


disable notifications package

disable notifications package

--- lib/main.coffee
+++ lib/main.coffee
@@ -17,6 +17,11 @@
     CommandLogger = require './command-logger'
     CommandLogger.start()
     @subscriptions = new CompositeDisposable
+
+    atom.packages.onDidActivateInitialPackages ->
+      atom.packages.disablePackage("notifications")
+    atom.packages.onDidActivatePackage (pkg) ->
+      atom.packages.disablePackage("notifications") if pkg is "notifications"

     @addNotificationView(notification) for notification in atom.notifications.getNotifications()
     @subscriptions.add atom.notifications.onDidAddNotification (notification) => @addNotificationView(notification)


information messages

information messages

--- lib/main.coffee
+++ lib/main.coffee
@@ -85,6 +85,29 @@

     sv = document.querySelector('.settings-view')

+    # Keybindings
+    info = sv.querySelector('.keybinding-panel>div:nth-child(2)')
+    info.querySelector('span:nth-child(2)').textContent = "これらのキーバインドは　"
+    info.querySelector('span:nth-child(4)').textContent = "をクリック（コピー）して"
+    info.querySelector('a.link').textContent = " キーマップファイル "
+    span = document.createElement('span')
+    span.textContent = "に貼り付けると上書きできます。"
+    info.appendChild(span)
+
+    # Themes panel
+    info = sv.querySelector('.themes-panel>div>div:nth-child(2)')
+    info.querySelector('span').textContent = "Atom は"
+    info.querySelector('a.link').textContent = " スタイルシート "
+    span = document.createElement('span')
+    span.textContent = "を編集してスタイルを変更することもできます。"
+    info.appendChild(span)
+    tp1 = sv.querySelector('.themes-picker>div:nth-child(1)')
+    tp1.querySelector('.setting-title').textContent = "インターフェーステーマ"
+    tp1.querySelector('.setting-description').textContent = "タブ、ステータスバー、ツリービューとドロップダウンのスタイルを変更します。"
+    tp2 = sv.querySelector('.themes-picker>div:nth-child(2)')
+    tp2.querySelector('.setting-title').textContent = "シンタックステーマ"
+    tp2.querySelector('.setting-description').textContent = "テキストエディタの内側のスタイルを変更します。"
+
     # Updates panel
     applySpecialHeading(sv, "Available Updates", 2, "利用可能なアップデート")
     applyTextWithOrg(sv.querySelector('.update-all-button.btn-primary'), "すべてアップデート")


Stacktrace.parse() returns multiple results now.

--- lib/main.coffee
+++ lib/main.coffee
@@ -11,9 +11,9 @@
     StacktraceView.registerIn(atom.workspace)

     atom.on 'stacktrace:accept-trace', ({trace}) =>
-      t = Stacktrace.parse(trace)
-      t.register()
-      atom.workspace.open t.getUrl()
+      for trace in Stacktrace.parse(trace)
+        trace.register()
+        atom.workspace.open trace.getUrl()

   deactivate: ->
     atom.off 'stacktrace:accept-trace'


Fix deprecated call

Fix deprecated call

--- lib/main.coffee
+++ lib/main.coffee
@@ -35,7 +35,7 @@
     if statusBar = document.querySelector('status-bar')
       createStatusBarView(statusBar)
     else
-      atom.packages.onDidActivateAll ->
+      atom.packages.onDidActivateInitialPackages ->
         statusBar = document.querySelector('status-bar')
         createStatusBarView(statusBar) if statusBar?


main.coffee: Modify to work with latest coala

--- lib/main.coffee
+++ lib/main.coffee
@@ -12,17 +12,18 @@
     # in coala the levels are: ["DEBUG", "INFO", "WARNING", "ERROR"]
     log_levels = ["Trace", "Info", "Warning", "Error"]
     provider =
-      grammarScopes: ['source.python']
+      grammarScopes: ['*']
       scope: 'file'
       lintOnFly: true
       lint: (textEditor)->
         filePath = textEditor.getPath()
         parameters = []
         parameters.push('--find-config')
+        parameters.push('-S=autoapply=false')
         parameters.push('--files=' + filePath)
         parameters.push('--settings')
         parameters.push("format_str=" +
-                        "R-{line_nr}-{severity}-{origin}:{message}")
+                        "R-{line}-{severity}-{origin}:{message}")
         return helpers.exec(atom.config.get('coala-atom.executable'),
                             parameters,
                             {cwd: path.dirname(filePath)})


Fix #732 courtesy of ssorallen++

Fix #732 courtesy of ssorallen++

--- lib/main.coffee
+++ lib/main.coffee
@@ -34,6 +34,7 @@
   consumeFileIcons: (service) ->
     FileIcons.setService(service)
     @fileIconsDisposable = service.onWillDeactivate -> FileIcons.resetService()
+    @treeView?.updateRoots()

   serialize: ->
     if @treeView?


Don't ignore php.ini

Don't ignore php.ini

I'm not sure why this was in there in the first place...

--- lib/main.coffee
+++ lib/main.coffee
@@ -45,7 +45,6 @@
         return Promise.resolve([]) unless command?
         parameters = []
         parameters.push('--syntax-check')
-        parameters.push('--no-php-ini')
         parameters.push('--define', 'display_errors=On')
         parameters.push('--define', 'log_errors=Off')
         text = textEditor.getText()


add console-panel fallback

add console-panel fallback

--- lib/main.coffee
+++ lib/main.coffee
@@ -30,6 +30,12 @@
       type: "boolean"
       default: true
       order: 3
+    clearConsole:
+      title: "Clear Console"
+      description: "When `console-panel` isn't available, build logs will be printed using `console.log()`. This setting clears the console prior to building."
+      type: "boolean"
+      default: true
+      order: 4
   subscriptions: null

   activate: (state) ->
@@ -73,18 +79,27 @@
       customArguments.push(script)
       args = defaultArguments.concat(customArguments)

-      consolePanel.clear()
+      try
+        consolePanel.clear()
+      catch
+        console.clear() if atom.config.get('language-nsl.clearConsole')

       # Let's go
       nslCmd = spawn "java", args
       hasError = false

       nslCmd.stdout.on 'data', (data) ->
-        consolePanel.log(data.toString()) if atom.config.get('language-nsl.alwaysShowOutput')
+        try
+          consolePanel.log(data.toString()) if atom.config.get('language-nsl.alwaysShowOutput')
+        catch
+          console.log(data.toString())

       nslCmd.stderr.on 'data', (data) ->
         hasError = true
-        consolePanel.error(data.toString())
+        try
+          consolePanel.error(data.toString()) if atom.config.get('language-nsl.alwaysShowOutput')
+        catch
+          console.error(data.toString())

       nslCmd.on 'close', ( errorCode ) ->
         if errorCode is 0 and hasError is false


Load Main on Startup

Load Main on Startup

--- lib/main.coffee
+++ lib/main.coffee
@@ -68,6 +68,8 @@
     if atom.config.get('atom-latex.hide_panel')
       @latex.panel.hidePanel()

+    @latex.manager.findMain(false)
+
   deactivate: ->
     @latex?.dispose()
     @disposables.dispose()


rename

rename

--- lib/main.coffee
+++ lib/main.coffee
@@ -79,10 +79,10 @@

     @subscriptions.add @onDidJumpToHistory (direction) =>
       @unLock()
-      @highlightCursorLine() if atom.config.get('cursor-history.flashOnJump')
+      @flashCursorLine() if atom.config.get('cursor-history.flashOnJump')
       @history.dump direction

-  highlightCursorLine: ->
+  flashCursorLine: ->
     activeEditor = @getActiveTextEditor()
     range = activeEditor.getSelectedBufferRange()
     marker = activeEditor.markBufferRange range,