Update theme.coffee

Update theme.coffee

Updated comment

--- server/models/theme.coffee
+++ server/models/theme.coffee
@@ -11,7 +11,7 @@
 )

 themeSchema.statics.seedData = (callback) ->
-  # Seed some indicators
+  # Seed some themes
   dummyThemes = JSON.parse(
     fs.readFileSync("#{process.cwd()}/lib/sample_themes.json", 'UTF8')
   )


Make sort/filter/limit parameters optional

--- server/publications.coffee
+++ server/publications.coffee
@@ -36,9 +36,9 @@
   Meteor.methods methods

   Houston._publish name, (sort, filter, limit) ->
-    check sort, Object
-    check filter, Object
-    check limit, Number
+    check sort, Match.Optional(Object)
+    check filter, Match.Optional(Object)
+    check limit, Match.Optional(Number)
     return unless Houston._user_is_admin @userId
     try
       collection.find(filter, sort: sort, limit: limit)


Tweak client doc syntax

Tweak client doc syntax

--- src/nack/client.coffee
+++ src/nack/client.coffee
@@ -6,6 +6,56 @@
 {EventEmitter}        = require 'events'
 {BufferedWriteStream} = require './buffered'

+# A **Client** establishes a connection to a worker process.
+#
+# It takes a `port` and `host` or a UNIX socket path.
+#
+# Its API is similar to `http.Client`.
+#
+#     var conn = client.createConnection('/tmp/nack.sock');
+#     var request = conn.request('GET', '/', {'host', 'localhost'});
+#     request.end();
+#     request.on('response', function (response) {
+#       console.log('STATUS: ' + response.statusCode);
+#       console.log('HEADERS: ' + JSON.stringify(response.headers));
+#       response.on('data', function (chunk) {
+#         console.log('BODY: ' + chunk);
+#       });
+#     });
+#
+exports.Client = class Client extends Stream
+  # Reconnect if the connection is closed.
+  reconnect: ->
+    if @readyState is 'closed'
+      @connect @port, @host
+
+  # Start the connection and create a ClientRequest.
+  request: (args...) ->
+    @reconnect()
+    request = new ClientRequest @, args...
+    request
+
+  # Proxy a `http.ServerRequest` and `http.serverResponse` between
+  # the `Client`.
+  proxyRequest: (serverRequest, serverResponse) ->
+    metaVariables =
+      "REMOTE_ADDR": serverRequest.connection.remoteAddress
+      "REMOTE_PORT": serverRequest.connection.remotePort
+
+    clientRequest = @request serverRequest.method, serverRequest.url, serverRequest.headers, metaVariables
+    sys.pump serverRequest, clientRequest
+
+    clientRequest.on "response", (clientResponse) ->
+      serverResponse.writeHead clientResponse.statusCode, clientResponse.headers
+      sys.pump clientResponse, serverResponse
+
+# Public API for creating a **Client**
+exports.createConnection = (port, host) ->
+  client = new Client
+  client.port = port
+  client.host = host
+  client
+
 # A **ClientRequest** is returned when `Client.request()` is called.
 #
 # It is a Writable Stream and responds to the conventional
@@ -13,11 +63,13 @@
 #
 # Its also an EventEmitter with the following events:
 #
-#   Event 'response'
-#   function (response) { }
-#   Emitted when a response is received to this request. This event is
-#   emitted only once. The response argument will be an instance of
-#   `ClientResponse`.
+# > **Event 'response'**
+# >
+# > `function (response) { }`
+# >
+# > Emitted when a response is received to this request. This event is
+# > emitted only once. The response argument will be an instance of
+# > `ClientResponse`.
 #
 exports.ClientRequest = class ClientRequest extends EventEmitter
   constructor: (@socket, @method, @path, headers, metaVariables) ->
@@ -42,7 +94,7 @@
   _parseEnv: (headers, metaVariables) ->
     @env = {}

-    # Set REQUEST_METHOD
+    # Set `REQUEST_METHOD`
     @env['REQUEST_METHOD'] = @method

     # Parse the request path an assign its parts to the env
@@ -51,12 +103,12 @@
     @env['QUERY_STRING'] = query
     @env['SCRIPT_NAME']  = ""

-    # Initialize REMOTE_ADDR and SERVER_ADDR to "0.0.0.0"
+    # Initialize `REMOTE_ADDR` and `SERVER_ADDR` to "0.0.0.0"
     # They can be overridden by `metaVariables`
     @env['REMOTE_ADDR'] = "0.0.0.0"
     @env['SERVER_ADDR'] = "0.0.0.0"

-    # Parse the HTTP_HOST header and set SERVER_NAME and SERVER_PORT
+    # Parse the `HTTP_HOST` header and set `SERVER_NAME` and `SERVER_PORT`
     if host = headers.host
       if {name, port} = headers.host.split ':'
         @env['SERVER_NAME'] = name
@@ -65,7 +117,7 @@
     for key, value of headers
       # Upcase all header key values
       key = key.toUpperCase().replace('-', '_')
-      # Prepend HTTP_ to them
+      # Prepend `HTTP_` to them
       key = "HTTP_#{key}" unless key == 'CONTENT_TYPE' or key == 'CONTENT_LENGTH'
       # And merge them into the `@env` obj
       @env[key] = value
@@ -88,18 +140,24 @@
 #
 # It is a Readable Stream and emits the conventional events:
 #
-#   Event: `data`
-#   function (chunk) { }
-#   Emitted when a piece of the message body is received.
-#
-#   Event: `end`
-#   function () { }
-#   Emitted exactly once for each message. No arguments. After emitted
-#   no other events will be emitted on the request.
-#
-#    Event: 'error'
-#    function (exception) { }
-#    Emitted when an error occurs.
+# > **Event: 'data'**
+# >
+# > `function (chunk) { }`
+# >
+# > Emitted when a piece of the message body is received.
+#
+# > **Event: 'end'**
+# >
+# > `function () { }`
+# >
+# > Emitted exactly once for each message. No arguments. After emitted
+# > no other events will be emitted on the request.
+# >
+# > **Event: 'error'**
+# >
+# > `function (exception) { }`
+# >
+# > Emitted when an error occurs.
 #
 exports.ClientResponse = class ClientResponse extends EventEmitter
   constructor: (@socket) ->
@@ -154,53 +212,3 @@
       @_stopped = true
       # Catch and emit as a socket error
       @socket.emit 'error', error
-
-# A **Client** establishes a connection to a worker process.
-#
-# It takes a `port` and `host` or a UNIX socket path.
-#
-# Its API is similar to `http.Client`.
-#
-#     var conn = client.createConnection('/tmp/nack.sock');
-#     var request = conn.request('GET', '/', {'host', 'localhost'});
-#     request.end();
-#     request.on('response', function (response) {
-#       console.log('STATUS: ' + response.statusCode);
-#       console.log('HEADERS: ' + JSON.stringify(response.headers));
-#       response.on('data', function (chunk) {
-#         console.log('BODY: ' + chunk);
-#       });
-#     });
-#
-exports.Client = class Client extends Stream
-  # Reconnect if the connection is closed.
-  reconnect: ->
-    if @readyState is 'closed'
-      @connect @port, @host
-
-  # Start the connection and create a ClientRequest.
-  request: (args...) ->
-    @reconnect()
-    request = new ClientRequest @, args...
-    request
-
-  # Proxy a `http.ServerRequest` and `http.serverResponse` between
-  # the `Client`.
-  proxyRequest: (serverRequest, serverResponse) ->
-    metaVariables =
-      "REMOTE_ADDR": serverRequest.connection.remoteAddress
-      "REMOTE_PORT": serverRequest.connection.remotePort
-
-    clientRequest = @request serverRequest.method, serverRequest.url, serverRequest.headers, metaVariables
-    sys.pump serverRequest, clientRequest
-
-    clientRequest.on "response", (clientResponse) ->
-      serverResponse.writeHead clientResponse.statusCode, clientResponse.headers
-      sys.pump clientResponse, serverResponse
-
-# Public API for creating a **Client**
-exports.createConnection = (port, host) ->
-  client = new Client
-  client.port = port
-  client.host = host
-  client


tower generate scaffold w/ new paths

tower generate scaffold w/ new paths

--- packages/tower-generator/server/helpers.coffee
+++ packages/tower-generator/server/helpers.coffee
@@ -10,14 +10,14 @@
     @inRoot =>
       if @controller.namespaced
         # @todo, add namespaces and such
-        @injectIntoFile "config/routes.coffee", "  #{routingCode}\n", after: /\.Route\.draw ->\n/, duplicate: false
+        @injectIntoFile "app/config/shared/routes.coffee", "  #{routingCode}\n", after: /\.Route\.draw ->\n/, duplicate: false
       else
-        @injectIntoFile "config/routes.coffee", "  #{routingCode}\n", after: /\.Route\.draw ->\n/, duplicate: false
+        @injectIntoFile "app/config/shared/routes.coffee", "  #{routingCode}\n", after: /\.Route\.draw ->\n/, duplicate: false

   bootstrap: (model) ->
     @inRoot =>
       # bootstrap into client side
-      @injectIntoFile "app/client/config/bootstrap.coffee",
+      @injectIntoFile "app/config/client/bootstrap.coffee",
         "  #{@app.namespace}.#{model.className}.load(data.#{model.namePlural}) if data.#{model.namePlural}\n",
           after: /bootstrap\ = *\(data\) *-\> *\n/i
           duplicate: false
@@ -29,12 +29,12 @@
         next()

 """
-      @injectIntoFile "app/controllers/applicationController.coffee", string, after: /_.series *\[ *\n/i, duplicate: false
+      @injectIntoFile "app/controllers/server/applicationController.coffee", string, after: /_.series *\[ *\n/i, duplicate: false

   asset: (path, options = {}) ->
     bundle = options.bundle || "application"
     @inRoot =>
-      @injectIntoFile "config/assets.coffee", "      \'#{path}\'\n",
+      @injectIntoFile "app/config/server/assets.coffee", "      \'#{path}\'\n",
         after: new RegExp("\\s*#{bundle}: *\\[[^\\]]*\\n", "i"),
         duplicate: false

@@ -47,11 +47,11 @@
 #"""

     @inRoot =>
-      @injectIntoFile "app/views/shared/_navigation.coffee", content, after: pattern, duplicate: false
+      @injectIntoFile "app/templates/shared/layout/_navigation.coffee", content, after: pattern, duplicate: false

   locale: (pattern, content) ->
     @inRoot =>
-      @injectIntoFile "config/locales/en.coffee", content, after: pattern, duplicate: false
+      @injectIntoFile "app/config/shared/locales/en.coffee", content, after: pattern, duplicate: false

   inRoot: (block) ->
     @inside ".", block


Making a js change to remove hub name from shipping method label

--- app/assets/javascripts/darkswarm/directives/active_table_hub_link.js.coffee
+++ app/assets/javascripts/darkswarm/directives/active_table_hub_link.js.coffee
@@ -2,7 +2,7 @@
   restrict: "A"
   scope:
     hub: '=activeTableHubLink'
-  template: "{{action}} <strong>{{hub.name}}</strong>"
+  template: "{{action}}"
   link: (scope, elm, attr)->
     # Swap out the text of the hub link depending on whether it'll change current hub
     # To be used with ofnEmptiesCart


Only retrieve required ElasticSearch fields

Cuts down on response body size.

--- src/scripts/control.search.js.coffee
+++ src/scripts/control.search.js.coffee
@@ -142,6 +142,7 @@
         q: "GH:#{query}"
         size: 100
         sort: "SISN:desc"
+        _source: "SISN,TI,DT,DA,AB,gh,location"
     }).done((results) =>
       @_addResults(@_filterResults(results.hits.hits))
       @searchBar.stopActivity()


Make Like tooltip sentences more polite

Make Like tooltip sentences more polite

--- client/app/Applications/Activity.kdapplication/views/activityactions.coffee
+++ client/app/Applications/Activity.kdapplication/views/activityactions.coffee
@@ -82,7 +82,7 @@
           else if activity.meta.likes is 3
             tooltip = peopleWhoLiked[0] + ", " + peopleWhoLiked[1] + " and " + peopleWhoLiked[2]
           else
-            tooltip = peopleWhoLiked.join(", ") + " and " + (activity.meta.likes - 3) + " more."
+            tooltip = peopleWhoLiked[0] + ", " + peopleWhoLiked[1] + " and " + (activity.meta.likes - 2) + " more."

           @likeCount.updateTooltip {title: tooltip }


ActivityAppController: Removed buggy docTitle logic for new activities.

--- client/landing/app/Applications/Activity.kdapplication/AppController.coffee
+++ client/landing/app/Applications/Activity.kdapplication/AppController.coffee
@@ -53,22 +53,6 @@
     @status.on "reconnected", (conn)=>
       if conn?.reason is "internetDownForLongTime" then @refresh()

-    @on "activitiesCouldntBeFetched", => @listController?.hideLazyLoader()
-
-    docTitle = document.title
-    KD.getSingleton("windowController").addFocusListener (blurred) =>
-      if blurred
-        @updateDocTitle()
-      else
-        document.title = docTitle
-
-      KD.getSingleton("activityController").on "ActivitiesArrived", => @updateDocTitle()
-
-  updateDocTitle: ->
-    KD.getSingleton("appManager").tell "Activity", "getNewItemsCount", (itemCount) =>
-      if itemCount > 0
-        document.title = "(#{itemCount}) Activity"
-
   loadView:->
     @getView().feedWrapper.ready (controller)=>
       @attachEvents @getView().feedWrapper.controller


Fixed comment for @markijbema

Fixed comment for @markijbema

--- core/app/assets/javascripts/base/placeholder_fix_ie.coffee
+++ core/app/assets/javascripts/base/placeholder_fix_ie.coffee
@@ -1,4 +1,4 @@
-# This only affects the inputs/textareas that aren't loaded through Backbone Views
+# This only affects the inputs/textareas that aren't loaded through Backbone Views because they don't exist here
 $ ->
   resetPlaceholders()


Fix weird coffeescript += and ?: issue.

Fix weird coffeescript += and ?: issue.

--- lib/event-watch-view.coffee
+++ lib/event-watch-view.coffee
@@ -73,8 +73,8 @@

     if hour == 12 && am
         hour = 0
-    else
-      hour += (hour < 12 && pm) ? 12 : 0
+    else if hour < 12 && pm
+      hour = hour + 12

     dt.setHours(hour)
     dt.setMinutes(minute)


Fix indentation

Fix indentation

--- components/SendRequest.coffee
+++ components/SendRequest.coffee
@@ -28,7 +28,7 @@
     req = http.request url

     req.end ->
-        c.outPorts.request.send req
+      c.outPorts.request.send req

   # Finally return the component instance
   c


Slave: Use pool's `shutdown` method during handoff

Removes a duplicate piece of code accomplishing the same thing.

--- src/streammachine/modes/slave.coffee
+++ src/streammachine/modes/slave.coffee
@@ -65,7 +65,7 @@
         @pool.on "full_strength", => @emit "full_strength"

         process.on "SIGTERM", =>
-            @pool.destroy (err) =>
+            @pool.shutdown (err) =>
                 @log.info "Pool destroyed."
                 process.exit()

@@ -189,41 +189,8 @@

             @log.info "Server socket transferred. Sending listener connections."

-            # now we ask each worker to send its listeners. We proxy them through
-            # to the new process, which in turn hands them off to its workers
-
-            _proxyWorker = (cb) =>
-                # are we done yet?
-                if @pool.count() == 0
-                    cb?()
-                    return false
-
-                # grab a worker id off the stack
-                w = @pool.getWorker()
-
-                @log.info "Starting transfer for worker #{w.id}"
-
-                w.rpc.request "shutdown", (err,msg) =>
-                    if err
-                        @log.error "Worker hit error during shutdown: #{err}", error:err, worker:w.id
-
-                    next = _.once =>
-                        # do it again...
-                        _proxyWorker cb
-
-
-                    t = setTimeout =>
-                        @log.error "Failed to get worker shutdown for #{w.id} (#{w.pid})"
-                        next()
-                    , 1000
-
-                    # we also want to see the process exit
-                    w.once "exit", =>
-                        clearTimeout t
-                        @log.info "Worker shutdown complete for #{w.id} (#{w.pid})"
-                        next()
-
-            _proxyWorker =>
+            # Ask the pool to shut down its workers.
+            @pool.shutdown (err) =>
                 @log.event "Sent slave data to new process. Exiting."

                 # Exit
@@ -390,11 +357,14 @@

         #----------

-        destroy: (cb) ->
+        shutdown: (cb) ->
             @_shutdown = true

             # send kill signals to all workers
             @log.info "Slave WorkerPool is exiting."
+
+            if @count() == 0
+                cb null

             af = _.after @count(), =>
                 cb null


Feeder: add defensive check for feedId

Feeder: add defensive check for feedId

--- client/Social/Feeder/FeedController.coffee
+++ client/Social/Feeder/FeedController.coffee
@@ -165,7 +165,7 @@
     options    = @getFeedOptions()
     selector   = @getFeedSelector()
     {itemClass, feedId}  = @getOptions()
-
+    feedId = "" unless feedId
     {groupsController} = KD.singletons

     if KD.config.entryPoint?.type is 'group'


mixpanel: use nickname to track, not firstname

--- client/landing/app/MainApp/mixpanel.coffee
+++ client/landing/app/MainApp/mixpanel.coffee
@@ -99,9 +99,9 @@
     if KD.isLoggedIn()
       user = KD.whoami()
       email = user.fetchEmail (err, email)->
-        mixpanel.identify user.profile.firstName
+        mixpanel.identify user.profile.nickname
         mixpanel.people.set
-          "$username"   : user.profile.firstName
+          "$username"   : user.profile.nickname
           "name"        : "#{user.profile.firstName} #{user.profile.lastName}"
           "$joinDate"   : user.meta.createdAt
           "$email"      : email


removed old obsolete code

removed old obsolete code

--- client/views/instances/instanceView.coffee
+++ client/views/instances/instanceView.coffee
@@ -1,5 +1,3 @@
-activeLogs = new ReactiveVar null
-
 isStateOk = (instance) ->
   if instance.meta.state is 'active'
     for i, service of instance.services
@@ -7,6 +5,21 @@
     true
   else
     false
+
+HTTPS_PORTS = ['443', '8443']
+HTTP_PORTS = ['80', '4567', '8000', '8080', '8081', '8181', '8668', '9000']
+
+findWebPort = (service) ->
+  p = 80
+  service?.ports?.replace(/[^\d|\s]/g, '').split(/\s+/).forEach (port) ->
+    if port in HTTPS_PORTS.concat(HTTP_PORTS) then p = port
+  p
+
+determineProtocol = (port) ->
+  if port in HTTPS_PORTS
+    "https"
+  else
+    "http"

 Template.instanceView.helpers
   activityIcon: ->
@@ -66,21 +79,3 @@
 Template.instanceView.onCreated ->
   new Clipboard '.copy-to-clipboard a',
     target: (trigger) -> trigger.parentNode.nextElementSibling
-
-Template.logsModal.helpers
-  logs: -> activeLogs.get()
-
-HTTPS_PORTS = ['443', '8443']
-HTTP_PORTS = ['80', '4567', '8000', '8080', '8081', '8181', '8668', '9000']
-
-findWebPort = (service) ->
-  p = 80
-  service?.ports?.replace(/[^\d|\s]/g, '').split(/\s+/).forEach (port) ->
-    if port in HTTPS_PORTS.concat(HTTP_PORTS) then p = port
-  p
-
-determineProtocol = (port) ->
-  if port in HTTPS_PORTS
-    "https"
-  else
-    "http"


bit.ly doesn't grab the title for urls that have a 301 redirect...

--- src/scripts/web.coffee
+++ src/scripts/web.coffee
@@ -19,8 +19,11 @@
       .get() (err, res, body) ->
         response = JSON.parse body
         responseTitle = response.data.info[0].title
-        console.log responseTitle if responseTitle
-        msg.send if response.status_code is 200 then response.data.info[0].title else response.status_txt
+        if responseTitle
+          msg.send if response.status_code is 200 then response.data.info[0].title else response.status_txt
+        else
+          msg.send "This bit.ly link doesn't have a title attribute set."
+          #grab longUrl, and retreive using http method

     #else print title
     else


ActivityInput: fixes 'Posting " " to Activity breaks' bug

--- client/Social/Activity/views/inputwidget.coffee
+++ client/Social/Activity/views/inputwidget.coffee
@@ -149,7 +149,7 @@
     @currentHelperNames = []

   submit: (callback) ->
-    return  unless value = @input.getValue().trim()
+    return @reset yes unless value = @input.getValue().trim()

     {JTag} = KD.remote.api


Client: Activity: Added setLastMessageEditMode actionType to message actions

--- client/activity/lib/flux/actions/message.coffee
+++ client/activity/lib/flux/actions/message.coffee
@@ -359,6 +359,12 @@

   { UNSET_MESSAGE_EDIT_MODE } = actionTypes
   dispatch UNSET_MESSAGE_EDIT_MODE, { messageId }
+
+
+setLastMessageEditMode = (accountId, __isEditing) ->
+
+  { SET_LAST_MESSAGE_EDIT_MODE } = actionTypes
+  dispatch SET_LAST_MESSAGE_EDIT_MODE, { accountId }

 module.exports = {
@@ -372,6 +378,9 @@
   loadComments
   createComment
   changeSelectedMessage
+  setMessageEditMode
+  unsetMessageEditMode
+  setLastMessageEditMode
   changeSelectedMessageBySlug
 }


activity: add wrapper to handle load message properly

--- client/activity/lib/flux/actions/message.coffee
+++ client/activity/lib/flux/actions/message.coffee
@@ -32,6 +32,22 @@
     kd.singletons.reactor.batch ->
       messages.forEach (message) ->
         dispatch LOAD_MESSAGE_SUCCESS, { channelId, channel, message }
+###*
+ * An action creator to group load message action operations.
+ * It wraps given message with realtimeActionCreators to transform realtime
+ * events to flux actions.
+ *
+ * @param {string} channelId
+ * @param {SocialMessage} message
+ * @api private
+###
+dispatchLoadMessageSuccess = (channelId, message) ->
+
+  channel = kd.singletons.socialapi.retrieveCachedItemById channelId
+
+  realtimeActionCreators.wrapMessage message
+  dispatch actionTypes.LOAD_MESSAGE_SUCCESS, { channelId, channel, message }
+

 ###*


payment: now the fee amount is coming in cents

--- client/app/MainApp/payment/vmproductview.coffee
+++ client/app/MainApp/payment/vmproductview.coffee
@@ -6,8 +6,7 @@
     """

{{ #(title) }}

-      $
-      {span{ @utils.formatMoney #(feeAmount) }}
+      {span{ @utils.formatMoney #(feeAmount) / 100 }}
       / mo

     """


docstring: It's not just possible, it's expected.

--- client/app/lib/providers/managed/helpers.coffee
+++ client/app/lib/providers/managed/helpers.coffee
@@ -81,7 +81,7 @@

   # A callback wrapper, to help ensure clearInterval is always called
   kallback = (err) ->
-    # It's possible that clearInterval gets called twice, but that
+    # It's expected that clearInterval gets called twice, but that
     # is not a problem in Chrome at least.
     clearInterval intervalId
     callback err


Return 405 Method Not Allowed in RECORD

Return 405 Method Not Allowed in RECORD

As per RFC 2326 11.3.1, server should return 405 Method Not Allowed if
RECORD request is issued when in PLAY mode.

--- rtsp.coffee
+++ rtsp.coffee
@@ -1427,11 +1427,11 @@
       @respondWithUnsupportedTransport callback, {CSeq: req.headers.cseq}
       return

-    mode = 'play'
+    client.mode = 'PLAY'
     if (match = /;mode=([^;]*)/.exec req.headers.transport)?
-      mode = match[1].toUpperCase()  # PLAY or RECORD
-
-    if mode is 'RECORD'
+      client.mode = match[1].toUpperCase()  # PLAY or RECORD
+
+    if client.mode is 'RECORD'
       sdpInfo = client.announceSDPInfo
       if (match = /\/([^/]+)$/.exec req.uri)?
         setupStreamId = match[1]  # e.g. "streamid=0"
@@ -1857,6 +1857,16 @@

   respondRecord: (socket, req, callback) ->
     client = @clients[socket.clientID]
+    if client.mode isnt 'RECORD'
+      logger.debug "client mode is not RECORD (got #{client.mode})"
+      res = """
+      RTSP/1.0 405 Method Not Allowed
+      CSeq: #{req.headers.cseq}
+
+
+      """.replace /\n/g, "\r\n"
+      return callback null, res
+
     streamId = RTSPServer.getStreamIdFromUri req.uri
     logger.info "[#{TAG}:client=#{client.id}] started uploading stream #{streamId}"
     stream = avstreams.getOrCreate streamId


Simplify removing find-mode history for incognito mode.

--- background_scripts/main.coffee
+++ background_scripts/main.coffee
@@ -661,15 +661,14 @@
 # We always remove chrome.storage.local/findModeRawQueryListIncognito on startup.
 chrome.storage.local.remove "findModeRawQueryListIncognito"

-# Remove chrome.storage.local/findModeRawQueryListIncognito if there are no remaining incognito-mode tabs.
+# Remove chrome.storage.local/findModeRawQueryListIncognito if there are no remaining incognito-mode windows.
 # Since the common case is that there are none to begin with, we first check whether the key is set at all.
 chrome.tabs.onRemoved.addListener (tabId) ->
   chrome.storage.local.get "findModeRawQueryListIncognito", (items) ->
     if items.findModeRawQueryListIncognito
-      chrome.windows.getAll { populate: true }, (windows) ->
+      chrome.windows.getAll null, (windows) ->
         for window in windows
-          for tab in window.tabs
-            return if tab.incognito and tab.id != tabId
+          return if window.incognito
         # There are no remaining incognito-mode tabs, and findModeRawQueryListIncognito is set.
         chrome.storage.local.remove "findModeRawQueryListIncognito"


Add backstop for global link hints.

Add backstop for global link hints.

This is not at all satisfactory.

I've seen cases where global link hints hangs, apparently because we do
not hear back from a frame.  The issue is repeatable when it happens,
but not repeatable in general (if you reload the page, then it goes
away).  This suggests that there may be something fundamentally wrong
with the global link-hints logic.  However, what?

In the interim, this adds a timer to trigger link-hints mode activation
with whataver hints we do have after a short period of time.

Ideally, we'll be able to get rid of this soon.

--- background_scripts/main.coffee
+++ background_scripts/main.coffee
@@ -367,7 +367,7 @@
         delete @tabState[tabId].ports[frameId]
         # It could be that we're expecting hints from this frame; schedule "sending" dummy/empty hints instead.
         Utils.nextTick =>
-          @postHintDescriptors tabId, frameId, {hintDescriptors: []} if frameId in tabState[tabId].frameIds
+          @postHintDescriptors tabId, frameId, hintDescriptors: []
     # We can delete the tab state when we see an "exit" message, that's the last message in the sequence.
     delete @tabState[tabId] if messageType == "exit"

@@ -377,14 +377,22 @@
     @tabState[tabId] = {frameIds: frameIdsForTab[tabId][..], hintDescriptors: [], originatingFrameId, modeIndex}
     @tabState[tabId].ports = extend {}, portsForTab[tabId]
     @sendMessage "getHintDescriptors", tabId, {modeIndex}
+    # FIXME(smblott)  This should not be necessary; it's a backstop to mitigate against the possibility that,
+    # for some reason, we do not hear back from a frame.
+    unless @debug
+      for frameId in frameIdsForTab[tabId]
+        do (frameId) =>
+          Utils.setTimeout 400, =>
+            @postHintDescriptors tabId, frameId, hintDescriptors: []

   # Receive hint descriptors from all frames and activate link-hints mode when we have them all.
   postHintDescriptors: (tabId, frameId, {hintDescriptors}) ->
-    @tabState[tabId].hintDescriptors.push hintDescriptors...
-    @tabState[tabId].frameIds = @tabState[tabId].frameIds.filter (fId) -> fId != frameId
-    console.log "postHintDescriptors", tabId, frameId, "[#{@tabState[tabId].frameIds.length}]" if @debug
-    if @tabState[tabId].frameIds.length == 0
-      @sendMessage "activateMode", tabId, @tabState[tabId]
+    if frameId in @tabState[tabId].frameIds
+      @tabState[tabId].hintDescriptors.push hintDescriptors...
+      @tabState[tabId].frameIds = @tabState[tabId].frameIds.filter (fId) -> fId != frameId
+      console.log "postHintDescriptors", tabId, frameId, "[#{@tabState[tabId].frameIds.length}]" if @debug
+      if @tabState[tabId].frameIds.length == 0
+        @sendMessage "activateMode", tabId, @tabState[tabId]

 # Port handler mapping
 portHandlers =


sets artist page test edge to new

sets artist page test edge to new

--- components/split_test/running_tests.coffee
+++ components/split_test/running_tests.coffee
@@ -27,4 +27,5 @@
     outcomes:
       old: 0.5
       new: 0.5
+    edge: 'new'
     dimension: 'dimension5'


don't show modal..

don't show modal..

--- lib/atom-lupa.coffee
+++ lib/atom-lupa.coffee
@@ -9,7 +9,7 @@
   activate: (state) ->
     console.log("lupa activate")
     plugin = require './init.coffee'
-
+
     # @atomLupaView = new AtomLupaView(state.atomLupaViewState)
     # @modalPanel = atom.workspace.addModalPanel(item: @atomLupaView.getElement(), visible: false)

@@ -30,7 +30,7 @@
   toggle: ->
     console.log 'AtomLupa was toggled!'

-    if @modalPanel.isVisible()
-      @modalPanel.hide()
-    else
-      @modalPanel.show()
+    # if @modalPanel.isVisible()
+    #   @modalPanel.hide()
+    # else
+    #   @modalPanel.show()


Center on selection on jump to bookmark

Center on selection on jump to bookmark

Fixes #32

--- lib/bookmarks.coffee
+++ lib/bookmarks.coffee
@@ -49,7 +49,8 @@
     bookmarkMarker = @[getBookmarkFunction](position.row)

     if bookmarkMarker
-      @editor.setSelectedBufferRange(bookmarkMarker.getBufferRange(), autoscroll: true)
+      @editor.setSelectedBufferRange(bookmarkMarker.getBufferRange(), autoscroll: false)
+      @editor.scrollToCursorPosition()
     else
       atom.beep()


Trigger destroyed event on TextBuffer

Trigger destroyed event on TextBuffer

This is useful for packages that are buffer-oriented and want to
unsubscribe when the buffer is no longer useable.

--- src/text-buffer.coffee
+++ src/text-buffer.coffee
@@ -81,6 +81,7 @@
       @unsubscribe()
       @destroyed = true
       @project?.removeBuffer(this)
+      @trigger 'destroyed'

   isRetained: -> @refcount > 0


Invisibles will be set in a call to updateInvisibles()

--- src/text-editor.coffee
+++ src/text-editor.coffee
@@ -84,7 +84,6 @@
     @cursors = []
     @selections = []

-    @displayBuffer?.setInvisibles(invisibles)
     @displayBuffer ?= new DisplayBuffer({buffer, tabLength, softWrapped})
     @buffer = @displayBuffer.buffer
     @softTabs = @usesSoftTabs() ? @softTabs ? atom.config.get('editor.softTabs') ? true


Don't change mergeIntersectingSelections behavior for now

--- src/text-editor.coffee
+++ src/text-editor.coffee
@@ -92,7 +92,7 @@

     @updateInvisibles()

-    @batch => @mergeIntersectingSelections =>
+    @batch =>
       for marker in @findMarkers(@getSelectionMarkerAttributes())
         marker.setProperties(preserveFolds: true)
         @addSelection(marker)
@@ -835,8 +835,7 @@
   #      argument will be a {Selection} and the second argument will be the
   #      {Number} index of that selection.
   mutateSelectedText: (fn) ->
-    @mergeIntersectingSelections => @transact =>
-      fn(selection, index) for selection, index in @getSelections()
+    @transact => fn(selection, index) for selection, index in @getSelections()

   # Move lines intersection the most recent selection up by one row in screen
   # coordinates.
@@ -995,7 +994,7 @@
   # selections to create multiple single-line selections that cumulatively cover
   # the same original area.
   splitSelectionsIntoLines: ->
-    @batch => @mergeIntersectingSelections =>
+    @batch =>
       for selection in @getSelections()
         range = selection.getBufferRange()
         continue if range.isSingleLine()
@@ -1803,7 +1802,7 @@
     @emitter.emit 'did-remove-cursor', cursor

   moveCursors: (fn) ->
-    @batch => @mergeIntersectingSelections =>
+    @batch =>
       fn(cursor) for cursor in @getCursors()
       @mergeCursors()


BUG For newer express

BUG For newer express

Express calls compile() not render()

--- lib/coffeekup.coffee
+++ lib/coffeekup.coffee
@@ -121,6 +121,16 @@

 coffeekup.compile = (template, options = {}) ->
   options.locals ?= {}
+
+  # Shim for express.
+  if options.locals.body?
+    options.context.body = options.locals.body
+    delete options.locals.body
+
+  if options.body?
+    options.context.body = options.body
+    delete options.body
+

   if typeof template is 'function' then template = String(template)
   else if typeof template is 'string' and coffee?
@@ -148,16 +158,11 @@
   new Function('ck_options', code)

 cache = {}
-
 coffeekup.render = (template, options = {}) ->
   options.context ?= {}
   options.locals ?= {}
   options.cache ?= on

-  # Shim for express.
-  if options.locals.body?
-    options.context.body = options.locals.body
-    delete options.locals.body

   if options.cache and cache[template]? then tpl = cache[template]
   else if options.cache then tpl = cache[template] = coffeekup.compile(template, options)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/text_effect.coffee
+++ src/text_effect.coffee
@@ -71,5 +71,4 @@
       y: 0
       text: I.text

-  return self
-
+  return self


rtsp: Disable warnings for unregistered UDP data

--- rtsp.coffee
+++ rtsp.coffee
@@ -424,9 +424,8 @@
       stream = @getStreamByRTSPUDPAddress rinfo.address, rinfo.port, 'video-data'
       if stream?
         @onUploadVideoData stream, msg, rinfo
-      else
-        logger.warn "[#{TAG}] warning: No uploading stream found for remote address #{rinfo.address}:#{rinfo.port} (udp)"
-        logger.warn "message: " + msg.toString 'utf8'
+#      else
+#        logger.warn "[#{TAG}] warn: received UDP video data but no existing client found: #{rinfo.address}:#{rinfo.port}"
     udpVideoDataServer.on 'listening', ->
       addr = udpVideoDataServer.address()
       logger.debug "[#{TAG}] udp video data receiver is listening on port #{addr.port}"
@@ -440,9 +439,8 @@
       stream = @getStreamByRTSPUDPAddress rinfo.address, rinfo.port, 'video-control'
       if stream?
         @onUploadVideoControl stream, msg, rinfo
-      else
-        logger.warn "[#{TAG}] warning: No uploading stream found for remote address #{rinfo.address}:#{rinfo.port} (udp)"
-        logger.warn "message: " + msg.toString 'utf8'
+#      else
+#        logger.warn "[#{TAG}] warn: received UDP video control data but no existing client found: #{rinfo.address}:#{rinfo.port}"
     udpVideoControlServer.on 'listening', ->
       addr = udpVideoControlServer.address()
       logger.debug "[#{TAG}] udp video control receiver is listening on port #{addr.port}"
@@ -456,9 +454,8 @@
       stream = @getStreamByRTSPUDPAddress rinfo.address, rinfo.port, 'audio-data'
       if stream?
         @onUploadAudioData stream, msg, rinfo
-      else
-        logger.warn "[#{TAG}] warning: No uploading stream found for remote address #{rinfo.address}:#{rinfo.port} (udp)"
-        logger.warn "message: " + msg.toString 'utf8'
+#      else
+#        logger.warn "[#{TAG}] warn: received UDP audio data but no existing client found: #{rinfo.address}:#{rinfo.port}"
     udpAudioDataServer.on 'listening', ->
       addr = udpAudioDataServer.address()
       logger.debug "[#{TAG}] udp audio data receiver is listening on port #{addr.port}"
@@ -472,9 +469,8 @@
       stream = @getStreamByRTSPUDPAddress rinfo.address, rinfo.port, 'audio-control'
       if stream?
         @onUploadAudioControl stream, msg, rinfo
-      else
-        logger.warn "[#{TAG}] warning: No uploading stream found for remote address #{rinfo.address}:#{rinfo.port} (udp)"
-        logger.warn "message: " + msg.toString 'utf8'
+#      else
+#        logger.warn "[#{TAG}] warn: received UDP audio control data but no existing client found: #{rinfo.address}:#{rinfo.port}"
     udpAudioControlServer.on 'listening', ->
       addr = udpAudioControlServer.address()
       logger.debug "[#{TAG}] udp audio control receiver is listening on port #{addr.port}"
@@ -1753,7 +1749,7 @@
   # Called when received video data over RTSP
   onUploadVideoData: (stream, msg, rinfo) ->
     if not stream.rtspUploadingClient?
-      logger.warn "no client is uploading video data to stream #{stream.id}"
+#      logger.warn "no client is uploading video data to stream #{stream.id}"
       return
     packet = rtp.parsePacket msg
     if not stream.rtspUploadingClient.videoRTPStartTimestamp?
@@ -1766,7 +1762,7 @@

   onUploadVideoControl: (stream, msg, rinfo) ->
     if not stream.rtspUploadingClient?
-      logger.warn "no client is uploading audio data to stream #{stream.id}"
+#      logger.warn "no client is uploading audio data to stream #{stream.id}"
       return
     packets = rtp.parsePackets msg
     for packet in packets
@@ -1778,7 +1774,7 @@

   onUploadAudioData: (stream, msg, rinfo) ->
     if not stream.rtspUploadingClient?
-      logger.warn "no client is uploading audio data to stream #{stream.id}"
+#      logger.warn "no client is uploading audio data to stream #{stream.id}"
       return
     packet = rtp.parsePacket msg
     if not stream.rtspUploadingClient.audioRTPStartTimestamp?
@@ -1791,7 +1787,7 @@

   onUploadAudioControl: (stream, msg, rinfo) ->
     if not stream.rtspUploadingClient?
-      logger.warn "no client is uploading audio data to stream #{stream.id}"
+#      logger.warn "no client is uploading audio data to stream #{stream.id}"
       return
     packets = rtp.parsePackets msg
     for packet in packets


array_extensions

--- src/array_extensions.coffee
+++ src/array_extensions.coffee
@@ -516,9 +516,7 @@

 @name sum
 @methodOf Array#
-
-@type Number
-@returns The sum of the elements in the array.
+@returns {Number} The sum of the elements in the array.
 ###
 Array::sum = ->
   this.inject 0, (sum, n) ->


Add initial size to window options when opening urls

--- src/atom-application.coffee
+++ src/atom-application.coffee
@@ -130,8 +130,7 @@
     @on 'application:hide-other-applications', -> Menu.sendActionToFirstResponder('hideOtherApplications:')
     @on 'application:unhide-all-applications', -> Menu.sendActionToFirstResponder('unhideAllApplications:')
     @on 'application:new-window', ->
-      [width, height] = @focusedWindow()?.getSize() ? []
-      @openPath(initialSize: {width, height})
+      @openPath(initialSize: @getFocusedWindowSize())
     @on 'application:new-file', -> (@focusedWindow() ? this).openPath()
     @on 'application:open', -> @promptForPath()
     @on 'application:open-dev', -> @promptForPath(devMode: true)
@@ -196,6 +195,17 @@
   # Public: Returns the currently focused {AtomWindow} or undefined if none.
   focusedWindow: ->
     _.find @windows, (atomWindow) -> atomWindow.isFocused()
+
+  # Public: Get the height and width of the focused window.
+  #
+  # Returns an object with height and width keys or null if there is no
+  # focused window.
+  getFocusedWindowSize: ->
+    if focusedWindow = @focusedWindow()
+      [width, height] = focusedWindow.getSize()
+      {width, height}
+    else
+      null

   # Public: Opens multiple paths, in existing windows if possible.
   #
@@ -282,7 +292,7 @@
       if pack.urlMain
         packagePath = @packages.resolvePackagePath(packageName)
         bootstrapScript = path.resolve(packagePath, pack.urlMain)
-        new AtomWindow({bootstrapScript, @resourcePath, devMode, urlToOpen})
+        new AtomWindow({bootstrapScript, @resourcePath, devMode, urlToOpen, initialSize: getFocusedWindowSize()})
       else
         console.log "Package '#{pack.name}' does not have a url main: #{urlToOpen}"
     else


ComputeController.UI: make title required in credential form

--- client/app/lib/providers/computecontroller.ui.coffee
+++ client/app/lib/providers/computecontroller.ui.coffee
@@ -107,6 +107,7 @@
         label        : 'Title'
         placeholder  : 'title for this credential'
         defaultValue : defaultTitle or ''
+        required     : yes

     if provider in ['custom', 'userInput'] and requiredFields
       credentialFields = {}


removed an assignment and inlined the result

--- src/commands/admin.coffee
+++ src/commands/admin.coffee
@@ -29,10 +29,8 @@
         validMap = msg.match[2] in [popularMaps, allMaps]
         filtered = maps.filter (map) -> map.indexOf(msg.match[2]) isnt -1

-        map = if filtered.length is 1 then filtered[0] else msg.random(popularMaps)
-
         new Lobby(
-          map,
+          if filtered.length is 1 then filtered[0] else msg.random(popularMaps),
           user,
           @brain.get('tflobby.servers')[@brain.get('tflobby.servers.default')]
         )


TODO removes as resolved

TODO removes as resolved

--- components/modules/Content/includes/js/script.coffee
+++ components/modules/Content/includes/js/script.coffee
@@ -65,7 +65,6 @@
 			'.cs-content-edit'
 			->
 				key = $(@).data('key')
-				# TODO HTML editor below not working
 				$.ajax(
 					url		: "api/Content/#{key}"
 					type	: 'get'


Add min width in vertical layout

Add min width in vertical layout

--- views/layout.vertical.cjsx
+++ views/layout.vertical.cjsx
@@ -18,6 +18,16 @@
   [].forEach.call $$('poi-app div.poi-app-tabpane'), (e) ->
     e.style.height = "#{window.innerHeight - (480.0 * factor - 1) - 70}px"
     e.style.overflowY = "scroll"
+  if window.webviewWidth > window.innerWidth
+    nowWindow = remote.getCurrentWindow()
+    bound = nowWindow.getBounds()
+    borderX = bound.width - window.innerWidth
+    newWidth = window.webviewWidth
+    nowWindow.setBounds
+      x: bound.x
+      y: bound.y
+      width: parseInt(newWidth + borderX)
+      height: bound.height
   $('kan-game #webview-wrapper')?.style?.height = $('kan-game webview')?.style?.height = $('kan-game webview /deep/ object[is=browserplugin]')?.style?.height = "#{480.0 * factor - 1}px"
   $('kan-game #webview-wrapper')?.style?.width = "#{800 * factor}px"
   $('kan-game #webview-wrapper')?.style?.marginLeft = "#{Math.max(0, window.innerWidth - 800 * factor - 1) / 2}px"


use backbone way for setting defaults

use backbone way for setting defaults

--- core/app/backbone/factlink/tooltip_definition.coffee
+++ core/app/backbone/factlink/tooltip_definition.coffee
@@ -10,10 +10,11 @@
 Backbone.Factlink ||= {}

 class Backbone.Factlink.TooltipDefinition
-  defaults = closingtimeout: 500
+  defaults:
+    closingtimeout: 500

   constructor: (options) ->
-    @_options = _.defaults options, defaults
+    @_options = _.defaults options, @defaults

   render: ->
     @_options.$container.hoverIntent


Improve context loss detection

Improve context loss detection

* Scan for focus changes

--- app/assets/rui/Attachment.coffee
+++ app/assets/rui/Attachment.coffee
@@ -28,6 +28,7 @@
       true

     document.addEventListener 'mousedown', @handleCloseRequest
+    document.addEventListener 'focus', @handleCloseRequest, true

     @tether = new Tether
       element: @node
@@ -49,6 +50,7 @@
     document.body.removeChild @node

     document.removeEventListener 'mousedown', @handleCloseRequest
+    document.removeEventListener 'focus', @handleCloseRequest

   renderComponent: ->
     @props.children
@@ -56,4 +58,7 @@
   render: ->
     span()

+Attachment.update = ->
+  Tether.position()
+
 module.exports = Attachment


Don't normalize path passed to synctex

Don't normalize path passed to synctex

--- app/coffee/CompileManager.coffee
+++ app/coffee/CompileManager.coffee
@@ -58,7 +58,7 @@
 		# might not match the file path on the host. The .synctex.gz file however, will be accessed
 		# wherever it is on the host.
 		base_dir = Settings.path.synctexBaseDir(project_id)
-		file_path = Path.join(base_dir, file_name)
+		file_path = base_dir + "/" + file_name
 		synctex_path = Path.join(Settings.path.compilesDir, project_id, "output.pdf")
 		CompileManager._runSynctex ["code", synctex_path, file_path, line, column], (error, stdout) ->
 			return callback(error) if error?


Fix wrong _.map usage on events service.

--- app/coffee/modules/events.coffee
+++ app/coffee/modules/events.coffee
@@ -72,7 +72,7 @@
         if not @.connected
             return

-        messages = _.map(@.serialize, @.pendingMessages)
+        messages = _.map(@.pendingMessages, @.serialize)
         @.pendingMessages = []

         for msg in messages


fix error stylizing when error is not a string

--- lib/rendering.coffee
+++ lib/rendering.coffee
@@ -303,20 +303,22 @@
 escapeSequences = /\u001b\[[0-9]?[A-Za-z]/mg

 cleanStdout = (s) ->
-  s = s.replace(sc3, '')
-  s = s.replace(escapeSequences, '')
+  if typeof s is 'string'
+    s = s.replace(sc3, '')
+    s = s.replace(escapeSequences, '')
   s

 stylizeErrors = (d) ->
-  d = d.replace(/^ERROR\:/gm,
-    """ERROR:""")
-  d = d.replace(/^WARNING\:/gm,
-    """WARNING:""")
-  # server errors
-  d = d.replace(/^FAILURE IN SERVER\:?/gm,
-    """FAILURE IN SERVER:""")
-  d = d.replace(/^\*\*\* ERROR/gm,
-    """*** ERROR""")
+  if typeof d is 'string'
+    d = d.replace(/^ERROR\:/gm,
+      """ERROR:""")
+    d = d.replace(/^WARNING\:/gm,
+      """WARNING:""")
+    # server errors
+    d = d.replace(/^FAILURE IN SERVER\:?/gm,
+      """FAILURE IN SERVER:""")
+    d = d.replace(/^\*\*\* ERROR/gm,
+      """*** ERROR""")
   d

 module.exports =


agnostic scroll for step-through

agnostic scroll for step-through

--- app/components/step-through.cjsx
+++ app/components/step-through.cjsx
@@ -59,6 +59,5 @@
       step: index %% total

   handleScroll: ->
-    node = ReactDOM.findDOMNode(@)
-    mediaCard = node.querySelector(".tutorial-steps div")
-    setTimeout (=> animatedScrollTo mediaCard, node.offsetTop, 0), 500
+    reactSwipeNode = ReactDOM.findDOMNode(@refs.swiper)
+    setTimeout (=> animatedScrollTo reactSwipeNode, reactSwipeNode.offsetTop, 0), 500


:lipstick:

:lipstick:

--- lib/tree-view.coffee
+++ lib/tree-view.coffee
@@ -215,7 +215,7 @@
         fullPath: projectPath
         symlink: false
         isRoot: true
-        expandedEntries,
+        expandedEntries
         isExpanded: true
       })
       @root = new DirectoryView()


Extend highlight span to be entire width of tree

Closes #219

--- lib/tree-view.coffee
+++ lib/tree-view.coffee
@@ -56,6 +56,7 @@
   afterAttach: (onDom) ->
     @focus() if @focusAfterAttach
     @scrollTop(@scrollTopAfterAttach) if @scrollTopAfterAttach > 0
+    @find('.selected > .highlight').width(@treeViewList[0].scrollWidth)

   serialize: ->
     directoryExpansionStates: @root?.serializeEntryExpansionStates()
@@ -304,10 +305,11 @@
     entry = entry.view() unless entry instanceof View
     @selectedPath = entry.getPath()
     @deselect()
+    entry.children('.highlight').width(@treeViewList[0].scrollWidth)
     entry.addClass('selected')

   deselect: ->
-    @treeViewList.find('.selected').removeClass('selected')
+    @treeViewList.find('.selected').removeClass('selected').children('.highlight').width('')

   scrollTop: (top) ->
     if top


Continue controller rewrite.

Continue controller rewrite.

--- src/controller.new.coffee
+++ src/controller.new.coffee
@@ -1,6 +1,7 @@
 define ['ice-coffee', 'ice-draw', 'ice-model'], (coffee, draw, model) ->
   # # Magic constants
   PALETTE_TOP_MARGIN = 5
+  PALETTE_MARGIN = 5
   MIN_DRAG_DISTANCE = 5
   PALETTE_LEFT_MARGIN = 5
   PALETTE_WIDTH = 300
@@ -15,7 +16,7 @@
   #
   # These are different events associated with the Editor
   # that features will want to bind to.
-  editorBindings = {
+  unsortedEditorBindings = {
     'populate': []

     'resize': []
@@ -26,9 +27,31 @@
     'mousemove': []
     'mouseup': []

-    'keydown': []
-    'keyup': []
+    'mutation': []
+
+    'key': []
   }
+
+  editorBindings = {}
+
+  # This hook function is for convenience,
+  # for features to add events that will occur at
+  # various times in the editor lifecycle.
+  hook = (event, priority, fn) ->
+    if event.indexOf('.') > 0
+      console.log event[...event.indexOf('.')]
+      unsortedEditorBindings[event[...event.indexOf('.')]].push {
+        priority: priority
+        fn: {
+          combo: event[event.indexOf('.') + 1..]
+          fn: fn
+        }
+      }
+    else
+      unsortedEditorBindings[event].push {
+        priority: priority
+        fn: fn
+      }

   # # The Editor Class
   exports.Editor = class Editor
@@ -45,7 +68,7 @@

       # We give our element a tabIndex so that it can be focused and capture keypresses.
       @iceElement.tabIndex = 0
-
+
       # Append that div.
       @wrapperElement.appendChild @iceElement

@@ -56,7 +79,7 @@
       @tracker.className = 'ice-track-area'

       # Append that div.
-      @wrapperElement.appendChild @tracker
+      @iceElement.appendChild @tracker

       # ## Canvases
       # Create the palette and main canvases
@@ -67,7 +90,7 @@

       @mainCtx = @mainCanvas.getContext '2d'

-      @wrapperElement.appendChild @mainCanvas
+      @iceElement.appendChild @mainCanvas

       # Then palette canvas
       @paletteCanvas = document.createElement 'canvas'
@@ -75,7 +98,7 @@

       @paletteCtx = @paletteCanvas.getContext '2d'

-      @wrapperElement.appendChild @paletteCanvas
+      @iceElement.appendChild @paletteCanvas

       # Call all the feature bindings that are supposed
       # to happen now.
@@ -86,7 +109,9 @@
       # This stage of ICE editor construction, which is repeated
       # whenever the editor is resized, should adjust the sizes
       # of all the ICE editor componenents to fit the wrapper.
-      window.addEventListener 'resize', @resize
+      window.addEventListener 'resize', =>
+        @resize()
+        @redrawMain(); @redrawPalette()

       @resize()

@@ -103,6 +128,14 @@
           # Call all the handlers.
           for handler in editorBindings[eventName]
             handler.call this, trackPoint, event, state
+
+      # We also allow binding to keypresses in the element.
+      # We will use dmauro's Keypress library for keyboard-shortcut
+      # input.
+      @keyListener = new window.keypress.Listener @iceElement
+      for listener in editorBindings.key then do (listener) =>
+        @keyListener.simple_combo listener.combo, =>
+          listener.fn.call this

       # # Document initialization
       # We start of with an empty document
@@ -117,8 +150,9 @@

       @mainCanvas.style.height = "#{@mainCanvas.height}px"
       @mainCanvas.style.width = "#{@mainCanvas.width}px"
-
-      @paletteCanvas.height = @iceElement.height - @paletteHeaderHeight
+
+      @paletteCanvas.style.top = @paletteHeaderHeight
+      @paletteCanvas.height = @iceElement.offsetHeight - @paletteHeaderHeight
       @paletteCanvas.width = PALETTE_WIDTH

       @paletteCanvas.style.height = "#{@paletteCanvas.height}px"
@@ -257,12 +291,165 @@
     else return null

   ###########################################
+  # UNDO STACK SUPPORT
+  ###########################################
+
+  # We must declare a few
+  # fields a populate time
+  hook 'populate', 0, ->
+    @undoStack = []
+
+  # The UndoOperation class is the base
+  # class for all undo operations.
+  class UndoOperation
+    constructor: ->
+
+    # Undo and redo should return
+    # the desired new cursor position.
+    undo: (editor) -> editor.tree.start
+    redo: (editor) -> editor.tree.start
+
+  # addMicroUndoOperation pushes
+  # the given operation to the undo stack,
+  # and might possibly do some bureaucracy in the future.
+  Editor::addMicroUndoOperation = (operation) ->
+    @undoStack.push operation
+
+    # We allow binding to mutation;
+    # all mutation must call addMicroUndoOperation.
+    for binding in editorBindings.mutation
+      binding.call this
+
+  # The undo function pops and undoes
+  # operations from the undo stack until
+  # we reach a capture point.
+  Editor::undo = ->
+    # If the undo stack is empty, give up
+    if @undoStack.length is 0 then return
+
+    # Otherwise, try to undo the operation
+    operation = @undoStack.pop()
+
+    # If the operation is a capture point, stop.
+    #
+    # NOTE: Right now capture points are signified by the
+    # string 'CAPTURE_POINT'; this may need to be changed
+    # in the future.
+    if operation is 'CAPTURE_POINT' then return
+    else
+      # Otherwise, apply the undo operation and
+      # recurse.
+      @moveCursorTo operation.undo this
+      @undo()
+
+    @redrawMain()
+
+  # We'll also allow users to clear
+  # the undo stack.
+  Editor::clearUndoStack = ->
+    @undoStack.length = 0
+
+  # Now we hook to ctrl-z to undo.
+  hook 'key.ctrl z', 0, ->
+    @undo()
+
+  ###########################################
   # BASIC BLOCK MOVE SUPPORT
   ###########################################
+
+  # Set up the undo operation for
+  # block moving.
+  class PickUpOperation extends UndoOperation
+    constructor: (block) ->
+      @block = block.clone()
+      beforeToken = block.start.prev
+
+      # For the "before" location, we don't actually want
+      # the thing before; we want the place at which we can
+      # place the block to get it back to what it used to be.
+      while beforeToken?.prev? and beforeToken.type in ['newline', 'segmentStart', 'cursor']
+        beforeToken = beforeToken.prev
+
+      @before = beforeToken?.getSerializedLocation() ? null
+
+    undo: (editor) ->
+      # If the block used to be at null, we don't need to do anything.
+      unless @before? then return
+
+      console.log editor.tree.getTokenAtLocation @before
+
+      # Move a clone into position.
+      (clone = @block.clone()).moveTo editor.tree.getTokenAtLocation @before
+
+      # Move the cursor to the end of it.
+      return clone.end
+
+    redo: (editor) ->
+      # If the operation was a no-op, redo is a no-op.
+      unless @before? then return
+
+      # Find the block we want to remove
+      blockStart = editor.tree.getTokenAtLocation @before
+      until blockStart.type is @block.start.type then blockStart = blockStart.next
+
+      # Move it to null.
+      if @block.start.type is 'segment'
+        blockStart.segment.moveTo null
+
+      else
+        blockStart.block.moveTo null
+
+      # Move the cursor somewhere close to what we
+      # just deleted.
+      return editor.tree.getTokenAtLocation @before
+
+  class DropOperation extends UndoOperation
+    constructor: (block, dest) ->
+      @block = block.clone()
+      @dest = dest?.getSerializedLocation() ? null
+
+      if dest?.type is 'socketStart'
+        @displacedSocketText = dest.socket.content()?.clone() ? null
+      else @displacedSocketText = null
+
+    undo: (editor) ->
+      # If the operation was a no-op, undo is a no-op.
+      unless @dest? then return
+
+      # Find the block we want to remove
+      blockStart = editor.tree.getTokenAtLocation @dest
+      until blockStart.type is @block.start.type then blockStart = blockStart.next
+
+      # Move it to null.
+      if @block.start.type is 'segment'
+        blockStart.segment.moveTo null
+
+      else
+        blockStart.block.moveTo null
+
+      # We may need to replace some of displaced
+      # socket text from dropping a block
+      # into a socket. If so, do so.
+      if @displacedSocketText?
+        editor.tree.getTokenAtLocation(@dest).insert @displacedSocketText.clone()
+
+      # Move the cursor somewhere close to what we
+      # just deleted.
+      return editor.tree.getTokenAtLocation @dest
+
+    redo: (editor) ->
+      # If the operation was a no-op, redo is a no-op.
+      unless @dest? then return
+
+      # Move a clone into position.
+      (clone = @block.clone()).moveTo editor.tree.getTokenAtLocation @dest
+
+      # Move the cursor to the end of it.
+      return clone.end

   # At population-time, we will
   # want to set up a few fields.
-  editorBindings.populate.push ->
+  hook 'populate', 0, ->
     @clickedPoint = null
     @clickedBlock = null

@@ -288,7 +475,7 @@
     @dragCtx.clearRect 0, 0, @dragCanvas.width, @dragCanvas.height

   # On resize, we will want to size the drag canvas correctly.
-  editorBindings.resize.push ->
+  hook 'resize', 0, ->
     @dragCanvas.width = @iceElement.offsetWidth - PALETTE_WIDTH
     @dragCanvas.height = @iceElement.offsetHeight

@@ -298,7 +485,10 @@
   #
   # We do not do anything until the user
   # drags their mouse five pixels
-  editorBindings.mousedown.push (point, event, state) ->
+  hook 'mousedown', 3, (point, event, state) ->
+    # If someone else has already taken this click, pass.
+    if state.consumedHitTest then return
+
     # Hit test against the tree.
     hitTestResult = hitTest @trackerPointToMain(point), @tree

@@ -319,7 +509,7 @@
   # If the user lifts the mouse
   # before they have dragged five pixels,
   # abort stuff.
-  editorBindings.mouseup.push (point, event, state) ->
+  hook 'mouseup', 0, (point, event, state) ->
     # @clickedBlock and @clickedPoint should will exist iff
     # we have dragged not yet more than 5 pixels.
     #
@@ -331,15 +521,37 @@
   # On mousemove, if there is a clicked block but no drag block,
   # we might want to transition to a dragging the block if the user
   # moved their mouse far enough.
-  editorBindings.mousemove.push (point, event, state) ->
+  hook 'mousemove', 1, (point, event, state) ->
     if @clickedBlock? and point.from(@clickedPoint).magnitude() > MIN_DRAG_DISTANCE
       # Signify that we are now dragging a block.
       @draggingBlock = @clickedBlock
-      @draggingOffset = @draggingBlock.view.bounds[@draggingBlock.view.lineStart].upperLeftCorner().from(
-        @trackerPointToMain(@clickedPoint))
-
-      # Take the block out of the tree
-      @moveBlockTo @draggingBlock, null
+
+      # Our dragging offset must be computed using the canvas on which this block
+      # is rendered.
+      #
+      # TODO this really falls under "PALETTE SUPPORT", but must
+      # go here. Try to organise this better.
+      if @clickedBlockIsPaletteBlock
+        @draggingOffset = @draggingBlock.view.bounds[@draggingBlock.view.lineStart].upperLeftCorner().from(
+          @trackerPointToPalette(@clickedPoint))
+
+        @draggingBlock = @draggingBlock.clone()
+
+        # Notice that since this block effectively
+        # came from nowhere, no undo operation
+        # is needed to destroy it.
+
+      else
+        @draggingOffset = @draggingBlock.view.bounds[@draggingBlock.view.lineStart].upperLeftCorner().from(
+          @trackerPointToMain(@clickedPoint))
+
+        # Since we removed this from the tree,
+        # we will need to log an undo operation
+        # to put it back in.
+        @addMicroUndoOperation new PickUpOperation @draggingBlock
+
+      # Remove the block from the tree.
+      @draggingBlock.moveTo null # MUTATION

       # Draw the new dragging block on the drag canvas.
       #
@@ -347,16 +559,28 @@
       # Also, we translate the block 1x1 to the right,
       # so that we can see its borders.
       @draggingBlock.view.compute(); @draggingBlock.view.translate new draw.Point 1, 1
-      @draggingBlock.view.drawShadow @dragCtx
+      @draggingBlock.view.drawShadow @dragCtx, 5, 5
       @draggingBlock.view.draw @dragCtx
+
+      # Translate it immediately into position
+      position = new draw.Point(
+        point.x + @draggingOffset.x,
+        point.y + @draggingOffset.y
+      )
+
+      @dragCanvas.style.top = "#{position.y}px"
+      @dragCanvas.style.left = "#{position.x}px"

       # Now we are done with the "clickedX" suite of stuff.
       @clickedPoint = @clickedBlock = null
+
+      # Redraw the main canvas
+      @redrawMain()

   # On mousemove, if there is a dragged block, we want to
   # translate the drag canvas into place,
   # as well as highlighting any focused drop areas.
-  editorBindings.mousemove.push (point, event, state) ->
+  hook 'mousemove', 0, (point, event, state) ->
     if @draggingBlock?
       # Translate the drag canvas into position.
       position = new draw.Point(
@@ -404,21 +628,26 @@

         @lastHighlight = highlight

-  editorBindings.mouseup.push (point, event, state) ->
+  hook 'mouseup', 0, (point, event, state) ->
     # We will consume this event iff we dropped it successfully
     # in the root tree.
     if @draggingBlock? and @lastHighlight?
       # Depending on what the highlighted element is,
       # we might want to drop the block at its
       # beginning or at its end.
+      #
+      # We will need to log undo operations here too.
       switch @lastHighlight.type
         when 'indent', 'socket'
-          @moveBlockTo @draggingBlock, @lastHighlight.start
+          @addMicroUndoOperation new DropOperation @draggingBlock, @lastHighlight.start
+          @draggingBlock.moveTo @lastHighlight.start #MUTATION
         when 'block'
-          @moveBlockTo @draggingBlock, @lastHighlight.end
+          @addMicroUndoOperation new DropOperation @draggingBlock, @lastHighlight.end
+          @draggingBlock.moveTo @lastHighlight.end #MUTATION
         else
           if @lastHighlight is @tree
-            @moveBlockTo @selection, @tree.start
+            @addMicroUndoOperation new DropOperation @draggingBlock, @tree.start
+            @selection.moveTo @tree.start #MUTATION

       # Now that we've done that, we can annul stuff.
       @draggingBlock = null
@@ -432,15 +661,55 @@
   # FLOATING BLOCK SUPPORT
   ###########################################

+  # We need to initialize the @floatingBlocks
+  # array at populate-time.
+  hook 'populate', 0, ->
+    @floatingBlocks = []
+
   class FloatingBlockRecord
     constructor: (@block, @position) ->
-
-  editorBindings.populate.push ->
-    @floatingBlocks = []
+
+  # # Undo operations
+  # We have two: FromFloating and ToFloating.
+  # They mimick block move pick/drop operations
+  # except that they also interact with the @floatingBlocks array.
+  class ToFloatingOperation extends DropOperation
+    constructor: (block, position) ->
+      # Copy the position we got
+      @position = new draw.Point position.x, position.y
+
+      # Do all the normal blockMove stuff.
+      super block, null
+
+    undo: (editor) ->
+      editor.floatingBlocks.pop()
+
+      super
+
+    redo: (editor) ->
+      editor.floatingBlocks.push new FloatingBlockRecord @block.clone(), @position
+
+      super
+
+  class FromFloatingOperation extends PickUpOperation
+    constructor: (record) ->
+      @position = new draw.Point record.position.x, record.position.y
+
+      super record.block, null
+
+    undo: (editor) ->
+      editor.floatingBlocks.push new FloatingBlockRecord @block.clone(), @position
+
+      super
+
+    redo: (editor) ->
+      editor.floatingBlocks.pop()
+
+      super

   # We can create floating blocks by dropping
   # blocks without a highlight.
-  editorBindings.mouseup.push (point, event, state) ->
+  hook 'mouseup', 0, (point, event, state) ->
     if @draggingBlock? and not @lastHighlight? and point.x > 0
       # Before we put this block into our list of floating blocks,
       # we need to figure out where on the main canvas
@@ -450,6 +719,10 @@
         mainCanvasPoint.x + @draggingOffset.x,
         mainCanvasPoint.y + @draggingOffset.y
       )
+
+      # Add the undo operation associated
+      # with creating this floating block
+      @addMicroUndoOperation new ToFloatingOperation @draggingBlock, renderPoint

       # Add this block to our list of floating blocks
       @floatingBlocks.push new FloatingBlockRecord(
@@ -466,7 +739,10 @@
       @redrawMain()

   # On mousedown, we can hit test for floating blocks.
-  editorBindings.mousedown.push (point, event, state) ->
+  hook 'mousedown', 7, (point, event, state) ->
+    # If someone else has already taken this click, pass.
+    if state.consumedHitTest then return
+
     # Hit test against floating blocks
     for record, i in @floatingBlocks
       hitTestResult = hitTest @trackerPointToMain(point), record.block
@@ -475,57 +751,399 @@
         @clickedBlock = record.block
         @clickedPoint = point

-        @floatingBlocks.splice i, 1
+        state.consumedHitTest = true

         @redrawMain()

+  hook 'mousemove', 7, (point, event, state) ->
+    if @clickedBlock? and point.from(@clickedPoint).magnitude() > MIN_DRAG_DISTANCE
+      for record, i in @floatingBlocks
+        if record.block is @clickedBlock
+          # Add the undo operation associated
+          # with picking up this floating block
+          @addMicroUndoOperation new FromFloatingOperation record
+
+          @floatingBlocks.splice i, 1
+
+          @redrawMain()
+
+          return
+
   # On redraw, we draw all the floating blocks
   # in their proper positions.
-  editorBindings.redraw_main.push ->
+  hook 'redraw_main', 0, ->
     for record in @floatingBlocks
       record.block.view.compute()
       record.block.view.translate record.position

       record.block.view.draw @mainCtx
+
+  ###########################################
+  # PALETTE SUPPORT
+  ###########################################
+
+  # The first thing we will have to do with
+  # the palette is install the hierarchical menu.
+  #
+  # This happens at population time.
+  hook 'populate', 0, ->
+    @currentPaletteBlocks = []
+    @clickedBlockIsPaletteBlock = false
+
+    # Create the hierarchical menu element.
+    @paletteHeader = document.createElement 'div'
+    @paletteHeader.className = 'ice-palette-header'
+
+    # Record its height, which is deterministic.
+    @paletteHeaderHeight = Math.ceil(@paletteGroups.length / 2) * 30
+
+    # Append the element.
+    @iceElement.appendChild @paletteHeader
+
+    for paletteGroup, i in @paletteGroups then do (paletteGroup) =>
+      # Clone all the blocks so as not to
+      # intrude on outside stuff
+      paletteGroup.blocks = (block.clone() for block in paletteGroup.blocks)
+
+      # Create the element itself
+      paletteGroupHeader = document.createElement 'div'
+      paletteGroupHeader.className = 'ice-palette-group-header'
+      paletteGroupHeader.innerText = paletteGroup.name
+
+      @paletteHeader.appendChild paletteGroupHeader
+
+      # When we click this element,
+      # we should switch to it in the palette.
+      paletteGroupHeader.addEventListener 'click', =>
+        # Record that we are the selected group now
+        @currentPaletteGroup = paletteGroup.name
+        @currentPaletteBlocks = paletteGroup.blocks
+
+        # Unapply the "selected" style to the current palette group header
+        @currentPaletteGroupHeader.className = 'ice-palette-group-header'
+
+        # Now we are the current palette group header
+        @currentPaletteGroupHeader = paletteGroupHeader
+
+        # Apply the "selected" style to us
+        @currentPaletteGroupHeader.className = 'ice-palette-group-header ice-palette-group-header-selected'
+
+        # Redraw the palette.
+        @redrawPalette()
+
+      # If we are the first element, make us the selected palette group.
+      if i is 0
+        @currentPaletteGroup = paletteGroup.name
+        @currentPaletteBlocks = paletteGroup.blocks
+
+        @currentPaletteGroupHeader = paletteGroupHeader
+
+        # Apply the "selected" style to us
+        @currentPaletteGroupHeader.className = 'ice-palette-group-header ice-palette-group-header-selected'
+
+  # The palette hierarchical menu is on top of the track div
+  # so that we can click it. However, we do not want this to happen
+  # when we are dragging something. So:
+  hook 'mousedown', 0, ->
+    @paletteHeader.style.zIndex = 0
+
+  hook 'mouseup', 0, ->
+    @paletteHeader.style.zIndex = 257
+
+  # The next thing we need to do with the palette
+  # is let people pick things up from it.
+  hook 'mousedown', 7, (point, event, state) ->
+    # If someone else has already taken this click, pass.
+    if state.consumedHitTest then return
+
+    palettePoint = @trackerPointToPalette point
+    for block in @currentPaletteBlocks
+      hitTestResult = hitTest palettePoint, block
+
+      if hitTestResult?
+        @clickedBlock = block
+        @clickedPoint = point
+        @clickedBlockIsPaletteBlock = true
+        return
+
+    @clickedBlockIsPaletteBlock = false
+
+  ###########################################
+  # TEXT INPUT SUPPORT
+  ###########################################
+
+  # Text input has two undo events: text change
+  # and text reparse.
+  class TextChangeOperation extends UndoOperation
+    constructor: (socket, @before) ->
+      @after = socket.stringify()
+      @socket = socket.start.getSerializedLocation()
+
+    undo: (editor) ->
+      socket = editor.tree.getTokenAtLocation(@socket).socket
+      socket.content()?.remove()
+      socket.start.insert new model.TextToken @before
+
+    redo: (editor) ->
+      socket = editor.tree.getTokenAtLocation(@socket).socket
+      socket.content()?.remove()
+      socket.start.insert new model.TextToken @after
+
+  class TextReparseOperation extends UndoOperation
+    constructor: (socket, @before) ->
+      @after = socket.content().clone()
+      @socket = socket.start.getSerializedLocation()
+
+    undo: (editor) ->
+      socket = editor.tree.getTokenAtLocation(@socket).socket
+      socket.content().moveTo null
+      socket.start.insert new model.TextToken @before
+
+    redo: (editor) ->
+      socket = editor.tree.getTokenAtLocation(@socket).socket
+      socket.content()?.remove()
+      @after.clone().moveTo socket
+
+  # At populate-time, we need
+  # to create and append the hidden input
+  # we will use for text input.
+  hook 'populate', 1, ->
+    @hiddenInput = document.createElement 'input'
+    @hiddenInput.className = 'hidden-input'
+
+    @iceElement.appendChild @hiddenInput
+
+    # We also need to initialise some fields
+    # for knowing what is focused
+    @socketFocus = null
+    @textFocus = null
+    @textInputAnchor = null
+
+    @textInputSelecting = false
+
+    @oldFocusValue = null
+
+    # The hidden input should be set up
+    # to mirror the text to which it is associated.
+    for event in ['input', 'keyup', 'keydown']
+      @hiddenInput.addEventListener event, =>
+        if @textFocus?
+          @textFocus.value = @hiddenInput.value
+
+          @redrawTextInput()
+
+  # Redraw function for text input
+  Editor::redrawTextInput = ->
+    # Set the value in the model to fit
+    # the hidden input value.
+    @textFocus.value = @hiddenInput.value
+
+    # Redraw the main canvas, on top of
+    # which we will draw the cursor and
+    # highlights.
+    @redrawMain()
+
+    # Determine the coordinate positions
+    # of the typic cursor
+    startPosition = @textFocus.view.bounds[@textFocus.view.lineStart].x +
+      @mainCtx.measureText(@hiddenInput.value[...@hiddenInput.selectionStart]).width
+    endPosition = @textFocus.view.bounds[@textFocus.view.lineStart].x +
+      @mainCtx.measureText(@hiddenInput.value[...@hiddenInput.selectionEnd]).width
+
+    # Now draw the highlight/typing cursor
+    #
+    # Draw a line if it is just a cursor
+    if startPosition is endPosition
+      @mainCtx.strokeRect startPosition, @textFocus.view.bounds[@textFocus.view.lineStart].y, 0, @fontSize
+
+    # Draw a translucent rectangle if there is a selection.
+    else
+      @mainCtx.fillStyle = 'rgba(0, 0, 256, 0.3)'
+      @mainCtx.fillRect startPosition, @textFocus.view.bounds[@textFocus.view.lineStart].y, endPosition - startPosition, @fontSize
+
+  # Convenince function for setting the text input
+  Editor::setTextInputFocus = (focus) ->
+    # If there is already a focus, we
+    # need to wrap some things up with it.
+    if @textFocus?
+
+      # The first of these is an undo operation;
+      # we need to add this text change to the undo stack.
+      @addMicroUndoOperation new TextChangeOperation @socketFocus, @oldFocusValue
+      @oldFocusValue = null
+
+      # The second of these is a reparse attempt.
+      # If we can, try to reparse the focus
+      # value.
+      try
+        newParse = coffee.parse(unparsedValue = @socketFocus.stringify()).start.next
+
+        if newParse.type is 'blockStart'
+          newParse.block.moveTo @socketFocus.start
+
+          @addMicroUndoOperation new TextReparseOperation @socketFocus, unparsedValue
+
+    # Now we're done with the old focus,
+    # we can start over.
+    #
+    # Literally set the focus.
+    @socketFocus = focus
+
+    # Record old focus value
+    @oldFocusValue = focus.stringify()
+
+    # Now create a text token
+    # with the appropriate text to put in it.
+    @textFocus = new model.TextToken focus.stringify()
+    focus.content()?.remove()
+    focus.start.insert @textFocus
+
+    # TODO move the cursor to the focus.
+
+    # Set the hidden input up to mirror the text.
+    @hiddenInput.value = @textFocus.value
+
+    # Focus the hidden input.
+    setTimeout (=> @hiddenInput.focus()), 0
+
+    # Redraw.
+    @redrawMain()
+
+  # Convenience hit-testing function
+  hitTestTextInput = (point, block) ->
+    head = block.start
+    while head?
+      if head.type is 'socketStart' and head.next.type in ['text', 'socketEnd'] and
+          head.socket.view.bounds[head.socket.view.lineStart].contains point
+        return head.socket
+      head = head.next
+
+    return null
+
+  # Convenience functions for setting
+  # the text input selection, given
+  # points on the main canvas.
+  Editor::setTextInputAnchor = (point) ->
+    @textInputAnchor = @textInputHead = Math.round(
+      (point.x - @textFocus.view.bounds[@textFocus.view.lineStart].x) / @mainCtx.measureText(' ').width
+    )
+
+    @hiddenInput.setSelectionRange @textInputAnchor, @textInputHead
+
+  Editor::setTextInputHead = (point) ->
+    @textInputHead = Math.round(
+      (point.x - @textFocus.view.bounds[@textFocus.view.lineStart].x) / @mainCtx.measureText(' ').width
+    )
+
+    @hiddenInput.setSelectionRange Math.min(@textInputAnchor, @textInputHead), Math.max(@textInputAnchor, @textInputHead)
+
+  # On mousedown, we will want to start
+  # selections and focus text inputs
+  # if we apply.
+  hook 'mousedown', 6, (point, event, state) ->
+    # If someone else already took this click, return.
+    if state.consumedHitTest then return
+
+    # Otherwise, look for a socket that
+    # the user has clicked
+    mainPoint = @trackerPointToMain point
+    hitTestResult = hitTestTextInput mainPoint, @tree
+
+    # If they have clicked a socket,
+    # focus it, and
+    if hitTestResult?
+      @setTextInputFocus hitTestResult
+
+      setTimeout (=>
+        @setTextInputAnchor mainPoint
+        @redrawTextInput()
+
+        @textInputSelecting = true
+      ), 0
+
+      state.consumedHitTest = true
+
+  # On mousemove, if we are selecting,
+  # we want to update the selection
+  # to match the mouse.
+  hook 'mousemove', 0, (point, event, state) ->
+    if @textInputSelecting
+      mainPoint = @trackerPointToMain point
+
+      @setTextInputHead mainPoint
+
+      @redrawTextInput()
+
+  # On mouseup, we want to stop selecting.
+  hook 'mouseup', 0, (point, event, state) ->
+    @textInputSelecting = false
+
+  ###########################################
+  # LASSO SELECT SUPPORT: TODO
+  ###########################################
+
+  ###########################################
+  # CURSOR OPERATION SUPPORT: TODO
+  ###########################################
+  Editor::moveCursorTo = ->
+
+  ###########################################
+  # HANDWRITTEN LINE SUPPORT: TODO
+  ###########################################
+
+  ###########################################
+  # ANIMATION SUPPORT: TODO
+  ###########################################

   ###########################################
   # SCROLLING SUPPORT: TODO
   ###########################################

-  editorBindings.populate.push ->
+  hook 'populate', 0, ->
     @scrollOffsets = {
       main: new draw.Point 0, 0
       palette: new draw.Point 0, 0
     }

   ###########################################
-  # PALETTE SUPPORT: TODO
-  ###########################################
-
-  editorBindings.populate.push ->
-    @currentPaletteBlocks = []
-
-  ###########################################
   # MULTIPLE FONT SIZE SUPPORT: TODO
   ###########################################
-  editorBindings.populate.push ->
+  hook 'populate', 0, ->
     @fontSize = 15

   ###########################################
-  # UNDO SUPPORT: TODO
-  ###########################################
-  Editor::moveBlockTo = (block, dest) ->
-    block.moveTo dest
-
-  Editor::clearUndoStack = ->
-
-  # # GET/SET VALUE
-  # Simple getters and setters for editor value.
+  # MUTATION BUTTON SUPPORT: TODO
+  ###########################################
+
+  ###########################################
+  # LINE MARKING SUPPORT: TODO
+  ###########################################
+
+  ###########################################
+  # LINE HOVER SUPPORT: TODO
+  ###########################################
+
+  ###########################################
+  # GET/SET VALUE SUPPORT: TODO
+  ###########################################

   Editor::setValue = (value) ->
     @tree = coffee.parse value
     @redrawMain()

   Editor::getValue = -> @tree.stringify()
-
+
+  ###########################################
+  # CLOSING FOUNDATIONAL STUFF
+  ###########################################
+
+  # Order the arrays correctly.
+  for key of unsortedEditorBindings
+    unsortedEditorBindings[key].sort (a, b) -> if a.priority > b.priority then -1 else 1
+
+    editorBindings[key] = []
+
+    for binding in unsortedEditorBindings[key]
+      editorBindings[key].push binding.fn
+
   return exports


DataHover now applies with the node as the scope

--- src/cuffs/bindings.coffee
+++ src/cuffs/bindings.coffee
@@ -193,9 +193,9 @@
             getArgs = =>
                 (context.get(arg) for arg in @funcArgs.split ',' when arg.trim())
             $(@node).mouseenter =>
-                context.get(@funcName, false)(true, getArgs()...)
+                context.get(@funcName, false).apply @node, [true].concat getArgs()
             $(@node).mouseleave =>
-                context.get(@funcName, false)(false, getArgs()...)
+                context.get(@funcName, false).apply @node, [false].concat getArgs()

     class DataSet extends Binding
         # Sets a certain value on the model. Sometimes useful. Eg:


body and headers preparations (lowercase & empty)

--- src/hit-validation.coffee
+++ src/hit-validation.coffee
@@ -21,9 +21,22 @@
       body: @responseBodyValidator.validate()
     }

+  prepareHeaders: (headers) ->
+    transformedHeaders = {}
+
+    for key, value of headers
+      transformedHeaders[key.toLowerCase()] = value.toLowerCase()
+
+    return transformedHeaders
+
+  prepareBody: (body) ->
+    if body == ''
+      return {}
+    return body
+
   getBodyValidator: (type) ->
-    if @hit[type].defined.schema?.body and  Object.keys(@hit[type].defined.schema?.body).length > 0
-      schema = @hit[type].defined.schema?.body
+    if @hit[type].defined.schema?.body and  Object.keys(JSON.parse @hit[type].defined.schema?.body).length > 0
+      schema = JSON.parse @hit[type].defined.schema?.body
     else
       try
         dataDefined = JSON.parse @hit[type].defined.body
@@ -32,6 +45,7 @@
         stj = new StringToJson @hit[type].defined.body
         dataDefined = stj.generate()
         schema = @getSchema data: dataDefined, type: 'string_body'
+      @hit[type].defined.schema?.body = JSON.stringify schema

     try
       dataReal = JSON.parse @hit[type].realPayload.body
@@ -43,11 +57,13 @@

   getHeadersValidator: (type) ->
     if @hit[type].defined.schema?.headers and Object.keys(@hit[type].defined.schema?.headers).length > 0
-      schema = @hit[type].defined.schema?.headers
+      schema = JSON.parse @hit[type].defined.schema?.headers
     else
-      schema = @getSchema data: @hit[type].defined.headers, type: 'headers'

-    return new Validator(data: @hit[type].realPayload.headers, schema: schema)
+      schema = @getSchema data: @prepareHeaders(@hit[type].defined.headers), type: 'headers'
+      @hit[type].defined.schema?.headers = JSON.stringify schema
+
+    return new Validator(data: @prepareHeaders(@hit[type].realPayload.headers), schema: schema)

   getSchema: ({data, type, properties})->
     properties = if properties then properties else new SchemaProperties


Remove debugging code

Remove debugging code

--- spec/javascripts/lib/core/ad_manager_spec.coffee
+++ spec/javascripts/lib/core/ad_manager_spec.coffee
@@ -1,4 +1,4 @@
-require ['jquery', 'public/assets/javascripts/lib/core/ad_manager'], ($, adManager) ->
+require ['public/assets/javascripts/lib/core/ad_manager'], (adManager) ->

   # Utility function to help setting up each test.
   adSetUp = (ad_types) ->


remove wrong comment

remove wrong comment

--- client/driver_lines.coffee
+++ client/driver_lines.coffee
@@ -153,7 +153,7 @@
 Template.driver_lines.events
   "click .insert": (event, template) ->
     event.preventDefault()
-    Session.set "typed_name",null # FIXME: use ReactiveVar not to spread to other browser windows?
+    Session.set "typed_name",null
     window.dialog_template = bootbox.dialog
       title: "Add a new driver line"
       message: window.renderTmp(Template.AddDriverLineDialog)


car_crashes: Fix placement of lower scatterplots

--- car_crashes/car_crashes.coffee
+++ car_crashes/car_crashes.coffee
@@ -5,7 +5,7 @@
     # sizes of things
     htop = 530
     hbot = 500
-    height = htop + hbot+200
+    height = htop + hbot

     margin_top = {left:5, top:20, right:25, bottom:40, inner:0}
     fullpanelwidth_top = 180
@@ -125,7 +125,7 @@
         scatterplots.push(this_scatterplot)

         hpos = fullpanelwidth_bot*col[i]
-        vpos = htop+row[i]*fullpanelheight_bot+margin_bot.top
+        vpos = htop+row[i]*fullpanelheight_bot
         this_g = svg.append("g")
                     .attr("class", "scatterplot")
                     .attr("id", "scatterplot#{i}")


metroid mode

metroid mode

--- src/javascript/app.coffee
+++ src/javascript/app.coffee
@@ -7,8 +7,8 @@

 MetroidCloneDelegate = require './game/metroid_clone_delegate'
 ShapesUiDelegate = require './game/shapes_ui_delegate'
-# DelegateClass = MetroidCloneDelegate
-DelegateClass = ShapesUiDelegate
+DelegateClass = MetroidCloneDelegate
+# DelegateClass = ShapesUiDelegate

 BigScreen = require './vendor/bigscreen_wrapper'
@@ -38,9 +38,14 @@
   harness = new PixiHarness
     domElement: gameHolder
     delegate: del
+    # width: 640
+    # height: 480
+    # zoom: 1.0
+
     width: 640
     height: 480
-    zoom: 1.0
+    zoom: 2.0
+
     # width: 320
     # height: 240
     # zoom: 1.0


read public key out of a file instead of keys.json; add private key path

--- code/lithium_config.coffee
+++ code/lithium_config.coffee
@@ -2,6 +2,7 @@

 exports.LithiumConfig = class LithiumConfig
   @sshkey_public: ->
-    text = fs.readFileSync "#{process.env['HOME']}/swops-secret/keys.json"
-    keys = JSON.parse text
-    keys['keys']['swops-public']
+    fs.readFileSync "#{process.env['HOME']}/swops-secret/id_dsa.pub", 'ascii'
+
+  @sshkey_private:
+    "#{process.env['HOME']}/swops-secret/id_dsa"


Fix missing translation

Fix missing translation

--- views/components/main/parts/kdock-panel.cjsx
+++ views/components/main/parts/kdock-panel.cjsx
@@ -141,9 +141,9 @@
           notified: notified
       when '/kcsapi/api_req_kousyou/createitem'
         if body.api_create_flag == 0
-          setTimeout warn.bind(@, __("The development of %s was failed.", "#{$slotitems[parseInt(body.api_fdata.split(',')[1])].api_name}")), showItemDevResultDelay
+          setTimeout warn.bind(@, __("The development of %s was failed.", "#{window.i18n.resources.__ $slotitems[parseInt(body.api_fdata.split(',')[1])].api_name}")), showItemDevResultDelay
         else if body.api_create_flag == 1
-          setTimeout success.bind(@, __("The development of %s was successful.", "#{$slotitems[body.api_slot_item.api_slotitem_id].api_name}")), showItemDevResultDelay
+          setTimeout success.bind(@, __("The development of %s was successful.", "#{window.i18n.resources.__ $slotitems[body.api_slot_item.api_slotitem_id].api_name}")), showItemDevResultDelay
   updateCountdown: ->
     {docks, notified} = @state
     for i in [1..4]


typos

typos

--- framer/Components/PageComponent.coffee
+++ framer/Components/PageComponent.coffee
@@ -153,7 +153,7 @@
 		# See if we meet the minimum velocity to scroll to the next page. If not we snap
 		# to the layer closest to the scroll point.
 		if Math.max(Math.abs(velocity.x), Math.abs(velocity.y)) < @velocityThreshold
-			print "velocity"
+			# print "velocity"
 			@snapToPage(@closestPage, true, @animationOptions)
 			return

@@ -165,8 +165,8 @@
 		# that we are already at.
 		nextPage ?= @closestPage

-		print Math.max(Math.abs(velocity.x), Math.abs(velocity.y))
-		print @direction, nextPage
+		# print Math.max(Math.abs(velocity.x), Math.abs(velocity.y))
+		# print @direction, nextPage

 		@snapToPage(nextPage, true, @animationOptions)


Restrict 'drop' based on 'next'

Restrict 'drop' based on 'next'

--- src/languages/html.coffee
+++ src/languages/html.coffee
@@ -359,11 +359,12 @@
 HTMLParser.parens = (leading, trailing, node, context) ->
   return [leading, trailing]

-HTMLParser.drop = (block, context, pred) ->
+HTMLParser.drop = (block, context, pred, next) ->

   blockType = block.classes[0]
   contextType = context.classes[0]
   predType = pred?.classes[0]
+  nextType = next?.classes[0]

   check = (blockType, allowList, forbidList = []) ->
     if blockType in allowList and blockType not in forbidList
@@ -372,7 +373,14 @@

   switch contextType
     when 'html'
-      return check blockType, ['head', 'body']
+      if blockType is 'head'
+        if predType is 'html' and (not next or nextType is 'body')
+          return helper.ENCOURAGE
+      if blockType is 'body'
+        if predType in ['html', 'head'] and not next
+          return helper.ENCOURAGE
+        return helper.FORBID
+      return helper.FORBID
     when 'head'
       return check blockType, METADATA_CONTENT
     when 'title'
@@ -533,8 +541,14 @@
       return check blockType, FLOW_CONTENT
     when '__segment'
       if blockType is '#documentType'
-        return check predType, [undefined, '__segment']
-      return check blockType, ['html']
+        if pred.type is 'segment' and (not next or nextType is 'html')
+          return helper.ENCOURAGE
+        return helper.FORBID
+      if blockType is 'html'
+        if (pred.type is 'segment' or predType is '#documentType') and not next
+          return helper.ENCOURAGE
+        return helper.FORBID
+      return helper.FORBID

   if contextType in HEADING_CONTENT
     return check blockType, PHRASING_CONTENT


fix assert

fix assert

--- src/layout/sprites.coffee
+++ src/layout/sprites.coffee
@@ -75,13 +75,12 @@

   perform: (world, action) ->
     [method, key, value] = action
-    console.log "perform world #{world}, method #{method}", method
     my.assert world[method], "#{world.label()}: no '#{method}' property"
     world[method](key, value)

   apply: (world, args) ->
     {target, action} = args
-    console.log "apply world #{world}, target #{target}"
+#    console.log "apply world #{world}, target #{target}"
     world.call('perform', action) if world == target

   step: (world, args) ->


Show review pane immediately after a problem has been solved.

--- web/coffee/problems.coffee
+++ web/coffee/problems.coffee
@@ -13,9 +13,12 @@
   input = $(e.target).find("input")
   apiCall "POST", "/api/problems/submit", {pid: input.data("pid"), key: input.val()}
   .done (data) ->
-    apiNotify(data)
     if data['status'] is 1
       loadProblems()
+      apiNotify(data)
+      setTimeout( ->
+        $("div[data-target='#" + input.data("pid") + "']").click()
+      , 100)

 addProblemReview = (e) ->
   e.preventDefault()


Remove PromiseRenderer from data export

Remove PromiseRenderer from data export

--- app/talk/data-export-button.cjsx
+++ app/talk/data-export-button.cjsx
@@ -1,20 +1,28 @@
 React = require 'react'
-PromiseRenderer = require '../components/promise-renderer'
 talkClient = require 'panoptes-client/lib/talk-client'
 moment = require 'moment'

 module.exports = React.createClass
   displayName: 'DataExportButton'
+
+  componentWillMount: ->
+    @exportGet()
+
+  componentWillReceiveProps: (newProps)->
+    @exportGet() if newProps.exportType isnt @props.exportType

   getInitialState: ->
     isAvailable: null
     exportRequested: false
     exportError: null
+    mostRecent: null

   exportGet: ->
     @_exportsGet or= talkClient.type('data_requests').get(section: @section(), kind: @props.exportType).then (requests) =>
-      @setState(isAvailable: true) if requests.length > 0
-      requests
+      if requests.length > 0
+        [mostRecent] = requests
+        isAvailable = true
+        @setState {mostRecent, isAvailable}

   section: ->
     "project-#{ @props.project.id }"
@@ -22,9 +30,11 @@
   requestDataExport: ->
     @setState exportError: null
     talkClient.type('data_requests').create(section: @section(), kind: @props.exportType).save()
-      .then =>
+      .then (data_request) =>
         @_exportsGet = null
-        @setState exportRequested: true
+        @setState
+          exportRequested: true
+          mostRecent: data_request
       .catch (error) =>
         @setState exportError: error

@@ -35,20 +45,19 @@
       </button> {' '}

         JSON format.{' '}
-        <PromiseRenderer promise={@exportGet()}>{([mostRecent]) =>
-          if mostRecent? and mostRecent?.url
-
-              Most recent data available requested {moment(mostRecent.created_at).fromNow()}:{' '}
-              Download.
-              Available until {moment(mostRecent.expires_at).calendar()}.
-
-          else if mostRecent?
-
-              Most recent data available requested {moment(mostRecent.created_at).fromNow()}:{' '}{mostRecent.state}
-
-          else
-            No recent requests
-        }</PromiseRenderer>
+        {if @state.mostRecent? and @state.mostRecent?.url
+
+            Most recent data available requested {moment(@state.mostRecent.created_at).fromNow()}:{' '}
+            Download.
+            Available until {moment(@state.mostRecent.expires_at).calendar()}.
+
+        else if @state.mostRecent?
+
+            Most recent data available requested {moment(@state.mostRecent.created_at).fromNow()}:{' '}{@state.mostRecent.state}
+
+        else
+          No recent requests
+        }


Ignore any releases that have a future scheduled release date.

--- compiler/ContentAPI.coffee
+++ compiler/ContentAPI.coffee
@@ -159,7 +159,12 @@

 class APIResults
     constructor: (array_of_results) ->
-        @_items = array_of_results
+        _now = new Date()
+        @_items = array_of_results.filter (item) ->
+            if item.scheduled_release_date
+                return new Date(item.scheduled_release_date) <= _now
+            else
+                return true
         @length = @_items.length

         @_items.forEach (item, i) =>


No need to run some tests twice.

No need to run some tests twice.

--- tests/html/src/tests.coffee
+++ tests/html/src/tests.coffee
@@ -277,8 +277,6 @@
         test "[unsupported by server]", ->
                 log('Unsupported protocol (by server): "' + protocol + '"')
     else
-        asyncTest("user close", factor_user_close(protocol))
-        asyncTest("server close", factor_server_close(protocol))
         asyncTest("invalid url 404", test_invalid_url_404(protocol))
         asyncTest("invalid url 500", test_invalid_url_500(protocol))
         asyncTest("invalid url port", test_invalid_url_port(protocol))


fix(feedback): Don't include tokens in feedback logs

--- internal_packages/inbox-activity-bar/lib/activity-bar.cjsx
+++ internal_packages/inbox-activity-bar/lib/activity-bar.cjsx
@@ -114,11 +114,17 @@

   _onFeedback: ->
     user = NamespaceStore.current().name
+
     debugData = JSON.stringify({
       queries: @state.curlHistory,
       queue: @state.queue,
       queue_pending: @state.queuePending
     }, null, '\t')
+
+    # Remove API tokens from URLs included in the debug data
+    # This regex detects ://user:pass@ and removes it.
+    debugData = debugData.replace(/:\/\/(\w)*:(\w)?@/g, '://')
+
     draft = new Message
       from: [NamespaceStore.current().me()]
       to: [


Trigger core:confirm on input view in replace spec

Refs #410

--- spec/operators-spec.coffee
+++ spec/operators-spec.coffee
@@ -997,15 +997,8 @@

     it "replaces a single character with a line break", ->
       keydown('r')
-      commandModeInputKeydown('\r')
+      editor.commandModeInputView.editor.trigger 'core:confirm'
       expect(editor.getText()).toBe '\n2\n34\n\n'
-
-    xit "replaces a single character with a line break", ->
-      keydown('r')
-      commandModeInputKeydown('\r')
-      expect(editor.getText()).toBe '\n2\n34\n\n'
-      # For some reason, the buffer position remains
-      # at [0, 0], even if it changes in an actual editor.
       expect(editor.getCursorBufferPosition()).toEqual [1, 0]

     it "composes properly with motions", ->


Revert "Revert "Add spec for inserting white-space-only lines""

This reverts commit fd17457c172838c0a919ff74944ba68ab9770391.

--- spec/selection-spec.coffee
+++ spec/selection-spec.coffee
@@ -83,3 +83,11 @@
       selection.setBufferRange([[2, 0], [2, 10]])
       selection.destroy()
       expect(selection.marker.isDestroyed()).toBeTruthy()
+
+  describe ".insertText(text, options)", ->
+    it "allows pasting white space only lines when autoIndent is enabled", ->
+      selection.setBufferRange [[0, 0], [0, 0]]
+      selection.insertText("    \n    \n\n", autoIndent: true)
+      expect(buffer.lineForRow(0)).toBe "    "
+      expect(buffer.lineForRow(1)).toBe "    "
+      expect(buffer.lineForRow(2)).toBe ""


DRY up vim-state-spec.coffee

DRY up vim-state-spec.coffee

--- spec/vim-state-spec.coffee
+++ spec/vim-state-spec.coffee
@@ -17,87 +17,81 @@
     options.element ?= editor[0]
     helpers.keydown(key, options)

-  describe "initialize", ->
+  describe "initialization", ->
     it "puts the editor in command-mode initially", ->
       expect(editor).toHaveClass 'vim-mode'
       expect(editor).toHaveClass 'command-mode'

   describe "command-mode", ->
-    it "stops propagation on key events would otherwise insert a character", ->
-      keydown('\\')
-      expect(editor.getText()).toEqual ''
+    describe "when entering an insertable character", ->
+      beforeEach -> keydown('\\')

-    # FIXME: See atom/vim-mode#2
-    xit "does not allow the cursor to be placed on the \n character, unless the line is empty", ->
-      editor.setText("012345\n\nabcdef")
-      editor.setCursorScreenPosition([0, 5])
-      expect(editor.getCursorScreenPosition()).toEqual [0, 5]
+      it "stops propagation", ->
+        expect(editor.getText()).toEqual ''

-      editor.setCursorScreenPosition([0, 6])
-      expect(editor.getCursorScreenPosition()).toEqual [0, 5]
+    describe "when entering an operator", ->
+      beforeEach -> keydown('d')

-      editor.setCursorScreenPosition([1, 0])
-      expect(editor.getCursorScreenPosition()).toEqual [1, 0]
+      describe "with an operator that can't be composed", ->
+        beforeEach -> keydown('x')

-    it "clears the operator stack when commands can't be composed", ->
-      keydown('d')
-      expect(vimState.opStack.length).toBe 1
-      keydown('x')
-      expect(vimState.opStack.length).toBe 0
+        it "clears the operator stack", ->
+          expect(vimState.opStack.length).toBe 0

-      keydown('d')
-      expect(vimState.opStack.length).toBe 1
-      keydown('\\')
-      expect(vimState.opStack.length).toBe 0
+      describe "the escape keybinding", ->
+        beforeEach -> keydown('escape')

-    describe "the escape keybinding", ->
-      it "clears the operator stack", ->
-        keydown('d')
-        expect(vimState.opStack.length).toBe 1
+        it "clears the operator stack", ->
+          expect(vimState.opStack.length).toBe 0

-        keydown('escape')
-        expect(vimState.opStack.length).toBe 0
+      describe "the ctrl-c keybinding", ->
+        beforeEach -> keydown('c', ctrl: true)

-    describe "the ctrl-c keybinding", ->
-      it "clears the operator stack", ->
-        keydown('d')
-        expect(vimState.opStack.length).toBe 1
-
-        keydown('c', ctrl: true)
-        expect(vimState.opStack.length).toBe 0
+        it "clears the operator stack", ->
+          expect(vimState.opStack.length).toBe 0

     describe "the i keybinding", ->
+      beforeEach -> keydown('i')
+
       it "puts the editor into insert mode", ->
-        expect(editor).not.toHaveClass 'insert-mode'
-
-        keydown('i')
-
         expect(editor).toHaveClass 'insert-mode'
         expect(editor).not.toHaveClass 'command-mode'

+    describe "with content", ->
+      beforeEach -> editor.setText("012345\n\nabcdef")
+
+      # FIXME: See atom/vim-mode#2
+      xdescribe "on a line with content", ->
+        beforeEach -> editor.setCursorScreenPosition([0, 6])
+
+        it "does not allow the cursor to be placed on the \n character", ->
+          expect(editor.getCursorScreenPosition()).toEqual [0, 5]
+
+      describe "on an empty line", ->
+        beforeEach -> editor.setCursorScreenPosition([1, 0])
+
+        it "allows the cursor to be placed on the \n character", ->
+          expect(editor.getCursorScreenPosition()).toEqual [1, 0]
+
   describe "insert-mode", ->
-    beforeEach ->
-      keydown('i')
+    beforeEach -> keydown('i')

-    it "allows the cursor to reach the end of the line", ->
-      editor.setText("012345\n\nabcdef")
-      editor.setCursorScreenPosition([0, 5])
-      expect(editor.getCursorScreenPosition()).toEqual [0, 5]
+    describe "with content", ->
+      beforeEach -> editor.setText("012345\n\nabcdef")

-      editor.setCursorScreenPosition([0, 6])
-      expect(editor.getCursorScreenPosition()).toEqual [0, 6]
+      describe "on a line with content", ->
+        beforeEach -> editor.setCursorScreenPosition([0, 6])
+
+        it "allows the cursor to be placed on the \n character", ->
+          expect(editor.getCursorScreenPosition()).toEqual [0, 6]

     it "puts the editor into command mode when <escape> is pressed", ->
-      expect(editor).not.toHaveClass 'command-mode'
-
       keydown('escape')

       expect(editor).toHaveClass 'command-mode'
       expect(editor).not.toHaveClass 'insert-mode'

     it "puts the editor into command mode when <ctrl-c> is pressed", ->
-      expect(editor).not.toHaveClass 'command-mode'
-
       keydown('c', ctrl: true)

       expect(editor).toHaveClass 'command-mode'


fix allocation model serialize

fix allocation model serialize

--- src/node_modules/allocation-model/allocation-model.coffee
+++ src/node_modules/allocation-model/allocation-model.coffee
@@ -9,7 +9,7 @@
     serialize: ->
       return {
         id: @id,
-        user: @user.serialize()
+        user_id: @user.id
         round_id: @roundId
         amount_cents: @amountCents
       }


Add title for recentlyUpdatedSongs page.

--- app/client/js/title.coffee
+++ app/client/js/title.coffee
@@ -17,6 +17,7 @@
   @MSG_ME: goog.getMsg 'Me'
   @MSG_NEW_SONG: goog.getMsg 'New Song'
   @MSG_NOT_FOUND: goog.getMsg 'Page Not Found'
+  @MSG_RECENTLY_UPDATED_SONGS: goog.getMsg 'Recently updated songs'
   @MSG_SONGS: goog.getMsg 'Songs'
   @MSG_TRASH: goog.getMsg 'Trash'

@@ -25,6 +26,7 @@
       when @routes.about then Title.MSG_ABOUT
       when @routes.home then Title.MSG_HOME
       when @routes.songs then Title.MSG_SONGS
+      when @routes.recentlyUpdatedSongs then Title.MSG_RECENTLY_UPDATED_SONGS
       when @routes.me then Title.MSG_ME
       when @routes.song then @getSongTitle @songsStore.songsByUrl[0]
       when @routes.mySong, @routes.editSong


also apply new logic to update method

also apply new logic to update method

--- app/controllers/top.coffee
+++ app/controllers/top.coffee
@@ -82,7 +82,10 @@
       unless @get('broadcasts.content').length
         @set('broadcasts.lastBroadcastStatus', '')
       else
-        @set('broadcasts.lastBroadcastStatus', @get('broadcasts.content')[0].category)
+        if @get('broadcasts.content').findBy('category', 'warning')
+          @set('broadcasts.lastBroadcastStatus', 'warning')
+        else if @get('broadcasts.content').findBy('category', 'announcement')
+          @set('broadcasts.lastBroadcastStatus', 'announcement')
       return false
   }