Use the underscore build of lodash

Use the underscore build of lodash

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -69,7 +69,7 @@
           'public/json2.js': 'components/json2/json2.js'
           'public/handlebars.runtime.js':
             'components/handlebars/handlebars.runtime.js'
-          'public/lodash.js': 'components/lodash/lodash.js'
+          'public/lodash.js': 'components/lodash/dist/lodash.underscore.js'
           'public/backbone.js': 'components/backbone/backbone.js'
           'public/backbone.shortcuts.js':
             'components/backbone.shortcuts/index.js'
@@ -88,7 +88,7 @@
             'components/keymaster/keymaster.js'
             'components/json2/json2.js'
             'components/handlebars/handlebars.runtime.js'
-            'components/lodash/lodash.js'
+            'components/lodash/dist/lodash.underscore.js'
             'components/backbone/backbone.js'
             'components/backbone.shortcuts/index.js'
             'public/templates.js'


Cleaning up Gruntfile

Cleaning up Gruntfile

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -28,7 +28,7 @@
                     transform: ['coffeeify', 'debowerify', ['browserify-plain-jade',
                         build: '<%= gitinfo.local.branch.current.shortSHA %>'
                         version: '<%= pkg.version %>'
-                        __debug: false
+                        __debug: no
                     ]]
                     browserifyOptions:
                         extensions: ['.coffee']
@@ -40,12 +40,12 @@
                     transform: ['coffeeify', ['browserify-plain-jade',
                         build: '<%= gitinfo.local.branch.current.shortSHA %>'
                         version: '<%= pkg.version %>'
-                        __debug: false
+                        __debug: no
                     ]]
                     external: js_dependencies
                     browserifyOptions:
                         extensions: ['.coffee']
-                        debug: true
+                        debug: yes

             libs:
                 dest: '<%= target_dir %>/libs.js'
@@ -53,7 +53,7 @@
                 options:
                     require: js_dependencies
                     browserifyOptions:
-                        debug: true
+                        debug: yes

         coffeelint:
             dist: ['<%= src_dir %>/scripts/**/*.coffee']
@@ -102,7 +102,8 @@
                 options:
                     data:
                         debug: no
-                        version: '<%= gitinfo.local.branch.current.shortSHA %>'
+                        version: '<%= pkg.version %>'
+                        build: '<%= gitinfo.local.branch.current.shortSHA %>'

             debug:
                 files:
@@ -110,6 +111,8 @@
                 options:
                     data:
                         debug: yes
+                        version: '<%= pkg.version %>'
+                        build: '<%= gitinfo.local.branch.current.shortSHA %>'

         uglify:
             dist:


Gruntfile config for multiple groups

Gruntfile config for multiple groups

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -34,6 +34,12 @@
           styles: ['test/dir/**/*.{css,less}']
         output: 'tmp/importless_output.less'
         template: 'test/importless_template.less'
+      index:
+        paths:
+          js: ['tmp/*.js']
+          less: ['tmp/*.less']
+        output: 'tmp/index_output.html'
+        template: 'test/index_template.html'

     clean: ['tmp/']


Fix on Gruntfile

Fix on Gruntfile

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -14,27 +14,18 @@
         'src/quo.coffee'
         'src/quo.*.coffee'
       ]
-      widgets: [
-        'src/gestures/quo.gestures.coffee'
-        'src/gestures/quo.gestures.*.coffee'
-      ]

     coffee:
-      app:      files: 'package/<%= meta.file %>.debug.js'        : ['<%= resources.app %>']
-      # widgets:  files: 'package/<%= meta.file %>.widget.debug.js' : ['<%= resources.widgets %>']
+      app: files: 'package/<%= meta.file %>.debug.js': ['<%= resources.app %>']

     uglify:
       options: compress: false, banner: "<%= meta.banner %>"
-      app:      files: 'package/<%= meta.file %>.js'        : 'package/<%= meta.file %>.debug.js'
-      # widgets:  files: 'package/<%= meta.file %>.widget.js' : 'package/<%= meta.file %>.widget.debug.js'
+      app: files: 'package/<%= meta.file %>.js': 'package/<%= meta.file %>.debug.js'

     watch:
       app:
         files: ['<%= resources.app %>']
         tasks: ["coffee:app"]
-      # widgets:
-      #   files: ['<%= resources.widgets %>']
-      #   tasks: ["coffee:widgets"]

   grunt.loadNpmTasks "grunt-contrib-coffee"
   grunt.loadNpmTasks "grunt-contrib-uglify"


Ensure that coffeescript files in folders are compiled as well

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -11,7 +11,7 @@
           bare: true
         expand: true
         cwd: './src/'
-        src: ['*.coffee']
+        src: ['**/*.coffee']
         dest: './lib/'
         ext: '.js'
     uglify:
@@ -29,7 +29,7 @@
         files: [
           expand: true
           cwd: './lib/'
-          src: ['*.js']
+          src: ['**/*.js']
           dest: './lib/'
           ext: '.js'
         ]


Update Gruntfile

Update Gruntfile

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -166,10 +166,10 @@
                     browsers: [
                         { browserName: 'chrome' }
                     ]
-            foobar:
-                src: ['static/test/insanity.js']
-                options:
-                    testName: 'webdriver test'
+            serviceMap:
+                src: ['static/test/webdriver-test.js']
+                options:
+                    testName: 'service map chrome test'
                     concurrency: 1
                     hostname: '127.0.0.1'
                     port: '4444'
@@ -180,20 +180,14 @@
                     browsers: [
                         { browserName: 'chrome' }
                     ]
-            firstTest:
+            phantomServiceMap:
                 src: ['static/test/webdriver-test.js']
                 options:
-                    testName: 'webdriver test'
-                    concurrency: 1
-                    hostname: '127.0.0.1'
-                    port: '4444'
-                    usePromises: false #default
-                    autoInstall: true
-                    # Firefox not working ?
-                    # https://github.com/ropensci/RSelenium/issues/42
-                    browsers: [
-                        { browserName: 'chrome' }
-                    ]
+                    testName: 'service map phantom test'
+                    usePhantom: true
+                    phantomPort: 5555
+                    reporter: 'spec'
+                    #usePromises: false
             seleniumPromises:
                 src: ['static/test/promiseAPI.js']
                 options:
@@ -318,4 +312,4 @@
     grunt.registerTask 'default', ['newer:coffee', 'newer:less', 'newer:i18next-yaml', 'newer:jade', 'newer:coffee2css']
     grunt.registerTask 'server', ['default', 'express', 'watch']
     grunt.registerTask 'tasks', ['coffee:tasks']
-    grunt.registerTask 'test', ['coffee:test', 'mochaWebdriver:firstTest']
+    grunt.registerTask 'test', ['coffee:test', 'mochaWebdriver']


fix: watch files in subdirectories for grunt tasks

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -44,7 +44,6 @@
       test:
         src: ["<%= less.serve.src %>"]

-    # Relax, you'll get lot of warnings or errors but most of them can be ignored. They're for reference only.
     validation:
       options:
         reset: true
@@ -82,7 +81,7 @@
           interrupt: true

       jekyll:
-        files: ["<%= config.app %>/**/*", "!_*",  "_config*.yml"]
+        files: ["<%= config.app %>/**/*", "!_*"]
         tasks: ['jekyll:serve']

     uglify:
@@ -93,7 +92,7 @@
         files: [
           expand: true
           cwd: "<%= config.app %>/assets/_js/"
-          src: ["*.js", "!*.min.js"]
+          src: ["**/*.js", "!*.min.js"]
           dest: "<%= config.app %>/assets/js/"
         ]

@@ -104,7 +103,7 @@
         files: [
           expand: true
           cwd: "<%= config.app %>/assets/_js/"
-          src: ["*.js", "!*.min.js"]
+          src: ["**/*.js", "!*.min.js"]
           dest: "<%= config.app %>/assets/js/"
         ]

@@ -187,7 +186,7 @@
         files: [
           expand: true
           cwd: "<%= config.dist %>/assets/css/"
-          src: ["*.css", "!*.min.css"]
+          src: ["**/*.css", "!*.min.css"]
           dest: "<%= config.dist %>/assets/css/"
         ]


Grunt: Use bash for kill

Grunt: Use bash for kill

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -99,7 +99,9 @@
       portallights_linux_gen: microflo_gen 'PortalLights'
       portallights_linux_comp: microflo_compile 'PortalLights'
       portallights_run: './spec/microflo-linux.sh PortalLights 5555 & sleep 5'
-      kill_microflo_linux: 'pkill microflo-linux || echo no processes to kill'
+      kill_microflo_linux:
+        options: { shell: '/bin/bash' }
+        command: 'pkill microflo-linux || echo no processes to kill'

   # Grunt plugins used for building
   @loadNpmTasks 'grunt-exec'


Don't need these

Don't need these

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -107,35 +107,19 @@
         files:
           "<%= dirs.dist %>/": ["<%= dirs.build %>/**"]

-        options:
-          cwd: ""
-          excludeEmpty: false
-
       zepto:
         files:
           "lib/zepto/": ["submodules/zepto/dist/**"]
           "<%= dirs.build %>/src/jqtouch-jquery.js": ["submodules/zepto/src/touch.js"]

-        options:
-          cwd: ""
-          excludeEmpty: false
-
       "jquery-bridge":
         files:
           "<%= dirs.build %>/src/jqtouch-jquery.js": ["submodules/zepto/src/touch.js"]
-
-        options:
-          cwd: ""
-          excludeEmpty: false

       checkin: # replace checkin version with updated css(es)
         files:
           "themes/css/": ["<%= dirs.dist %>/themes/css/**"]
           "src/jqtouch-jquery.js": ["<%= dirs.dist %>/src/jqtouch-jquery.js"]
-
-        options:
-          cwd: ""
-          excludeEmpty: false

     replace:
       "jquery-bridge":


cleanup

cleanup

--- lib/item-indicator.coffee
+++ lib/item-indicator.coffee
@@ -4,10 +4,7 @@
   constructor: (@ui) ->
     {@editor} = @ui
     @gutter = @editor.addGutter(name: 'narrow-item-indicator', priority: 100)
-
     @item = document.createElement('span')
-    # @item.className = 'icon icon-arrow-right'
-    # @item.textContent = " > "

   setToRow: (@row) ->
     @marker?.destroy()


Evaluate function values before running through formatters (unless they're for an event binding). [Closes #59]

--- src/rivets.coffee
+++ src/rivets.coffee
@@ -43,7 +43,10 @@
   # Sets the value for the binding. This Basically just runs the binding routine
   # with the suplied value formatted.
   set: (value) =>
-    value = @formattedValue value
+    value = if value instanceof Function and @options.special isnt "event"
+      @formattedValue value.call @model
+    else
+      @formattedValue value

     if @options.special is "event"
       @routine @el, value, @currentListener
@@ -51,7 +54,6 @@
     else if @options.special is "iteration"
       @routine @el, value, @
     else
-      value = value.call(@model) if value instanceof Function
       @routine @el, value

   # Subscribes to the model for changes at the specified keypath. Bi-directional


Исправил экспорт функции в браузер

Исправил экспорт функции в браузер

--- src/rubles.coffee
+++ src/rubles.coffee
@@ -101,4 +101,5 @@
   numeral += parseDecimals toFloat decimals if decimals
   numeral

-(exports or window).rubles = rubles+global = if module? then exports else window
+global.rubles = rubles


Check that runtime is still there between suites and cases, fixes #137

--- src/runner.coffee
+++ src/runner.coffee
@@ -181,6 +181,9 @@
     debug 'setup suite', "\"#{suite.name}\""
     return callback null if not needsSetup suite

+    unless @client.isConnected()
+      return callback new Error 'Disconnected from runtime'
+
     getFixtureGraph this, suite, (err, graph) =>
       return callback err if err
       protocol.sendGraph @client, graph, (err, graphId) =>
@@ -194,12 +197,18 @@
     debug 'teardown suite', "\"#{suite.name}\""
     return callback null if not needsSetup suite

+    unless @client.isConnected()
+      return callback new Error 'Disconnected from runtime'
+
     # FIXME: also remove the graph. Ideally using a 'destroy' message in FBP protocol
     protocol.stopNetwork @client, @currentGraphId, callback
     return

   runTest: (testcase, callback) ->
     debug 'runtest', "\"#{testcase.name}\""
+
+    unless @client.isConnected()
+      return callback new Error 'Disconnected from runtime'

     # XXX: normalize and validate in testsuite.coffee instead?
     inputs = if common.isArray(testcase.inputs) then testcase.inputs else [ testcase.inputs ]


Move some methods around in server.coffee

--- src/server.coffee
+++ src/server.coffee
@@ -52,24 +52,6 @@
 			node = new Node(@, socket)
 			@addNode(node)

-		# Sends a message to a certain node.
-		#
-		# @param to [String] the id of the node to pass the message to
-		# @param event [String] the event to pass to the node
-		# @param args... [Any] any arguments to pass along
-		#
-		emitTo: ( to, event, args... ) ->
-			message = new Message(to, null, event, args)
-			@relay(message)
-
-		# Relays a composed message to a certain node.
-		#
-		# @param message [Message] the message to relay
-		#
-		relay: ( message ) ->
-			if node = @getNode(message.to)
-				node.send(message)
-
 		# Adds a node to the node list
 		#
 		# @param node [Node] the node to add
@@ -102,6 +84,24 @@
 				return _(@_nodes).filter( ( node ) -> node.type is type )
 			else
 				return @_nodes
+
+		# Sends a message to a certain node.
+		#
+		# @param to [String] the id of the node to pass the message to
+		# @param event [String] the event to pass to the node
+		# @param args... [Any] any arguments to pass along
+		#
+		emitTo: ( to, event, args... ) ->
+			message = new Message(to, null, event, args)
+			@relay(message)
+
+		# Relays a composed message to a certain node.
+		#
+		# @param message [Message] the message to relay
+		#
+		relay: ( message ) ->
+			if node = @getNode(message.to)
+				node.send(message)

 		# Responds to a request
 		#


Change regex

Change regex

Change regex to find `TODO:` but also `TODO :`.

--- lib/task-list-view.coffee
+++ lib/task-list-view.coffee
@@ -14,9 +14,9 @@
     if editor
       ebuffer = editor.buffer
       if ebuffer
-        matcher = /TODO: (.*)|FIXME: (.*)/g
-        todoFinder = /TODO: (.*)/
-        fixmeFinder = /FIXME: (.*)/
+        matcher = /TODO ?: (.*)|FIXME ?: (.*)/g
+        todoFinder = /TODO ?: (.*)/
+        fixmeFinder = /FIXME ?: (.*)/
         todos = []
         if ebuffer.cachedText
           if ebuffer.cachedText.match(matcher)


Fix confusion of marker's head and tail with violations' start and end

--- lib/violation-view.coffee
+++ lib/violation-view.coffee
@@ -14,8 +14,8 @@
     @editor = @editorView.getEditor()

     screenRange = @editor.screenRangeForBufferRange(@violation.bufferRange)
-    @headScreenPosition = screenRange.start
-    @tailScreenPosition = screenRange.end
+    @startScreenPosition = screenRange.start
+    @endScreenPosition = screenRange.end

     @prepareTooltip()
     @trackEdit()
@@ -33,8 +33,14 @@
   trackEdit: ->
     @marker = @editor.markScreenRange(@getCurrentScreenRange(), invalidation: 'inside')
     @marker.on 'changed', ({newHeadScreenPosition, newTailScreenPosition, isValid}) =>
-      @headScreenPosition = newHeadScreenPosition
-      @tailScreenPosition = newTailScreenPosition
+      # Head and Tail: Markers always have a head and sometimes have a tail.
+      # If you think of a marker as an editor selection, the tail is the part that's stationary
+      # and the head is the part that moves when the mouse is moved.
+      # A marker without a tail always reports an empty range at the head position.
+      # A marker with a head position greater than the tail is in a "normal" orientation.
+      # If the head precedes the tail the marker is in a "reversed" orientation.
+      @startScreenPosition = newTailScreenPosition
+      @endScreenPosition = newHeadScreenPosition
       if isValid
         @violationTooltip('show')
       else
@@ -47,8 +53,8 @@
   toggleTooltipWithCursorPosition: ->
     cursorPosition = @editor.getCursor().getScreenPosition()

-    if cursorPosition.row is @headScreenPosition.row &&
-       cursorPosition.column is @headScreenPosition.column
+    if cursorPosition.row is @startScreenPosition.row &&
+       cursorPosition.column is @startScreenPosition.column
       @violationTooltip('show')
     else
       @violationTooltip('hide')
@@ -76,7 +82,7 @@
     @hide()

   getCurrentScreenRange: ->
-    new Range(@headScreenPosition, @tailScreenPosition)
+    new Range(@startScreenPosition, @endScreenPosition)

   violationTooltip: (option) ->
     violationView = this


fixed bug: "disabled" instead of "input" when checking for disabled attribute

--- lib/zombie/browser.coffee
+++ lib/zombie/browser.coffee
@@ -497,7 +497,7 @@
   fill: (selector, value, callback)->
     field = @field(selector)
     if field && (field.tagName == "TEXTAREA" || (field.tagName == "INPUT"))
-      if field.getAttribute("input")
+      if field.getAttribute("disabled")
         throw new Error("This INPUT field is disabled")
       if field.getAttribute("readonly")
         throw new Error("This INPUT field is readonly")


Use more functional code in DatetimeTickFormatter._get_resolution_str()

--- bokehjs/src/coffee/models/formatters/datetime_tick_formatter.coffee
+++ bokehjs/src/coffee/models/formatters/datetime_tick_formatter.coffee
@@ -95,31 +95,17 @@
     # numbers, as we've worked hard to ensure).  Consequently, we adjust the
     # resolution upwards a small amount (less than any possible step in
     # scales) to make the effective boundaries slightly lower.
-    adjusted_resolution_secs = resolution_secs * 1.1
-
-    if adjusted_resolution_secs < 1e-3
-      str = "microseconds"
-    else if adjusted_resolution_secs < 1.0
-      str = "milliseconds"
-    else if adjusted_resolution_secs < 60
-      if span_secs >= 60
-        str = "minsec"
-      else
-        str = "seconds"
-    else if adjusted_resolution_secs < 3600
-      if span_secs >= 3600
-        str = "hourmin"
-      else
-        str = "minutes"
-    else if adjusted_resolution_secs < 24*3600
-      str = "hours"
-    else if adjusted_resolution_secs < 31*24*3600
-      str = "days"
-    else if adjusted_resolution_secs < 365*24*3600
-      str = "months"
-    else
-      str = "years"
-    return str
+    adjusted_secs = resolution_secs * 1.1
+
+    return switch
+      when adjusted_secs < 1e-3        then "microseconds"
+      when adjusted_secs < 1.0         then "milliseconds"
+      when adjusted_secs < 60          then (if span_secs >= 60   then "minsec"  else "seconds")
+      when adjusted_secs < 3600        then (if span_secs >= 3600 then "hourmin" else "minutes")
+      when adjusted_secs < 24*3600     then "hours"
+      when adjusted_secs < 31*24*3600  then "days"
+      when adjusted_secs < 365*24*3600 then "months"
+      else                                  "years"

   doFormat: (ticks, num_labels=null, char_width=null, fill_ratio=0.3, ticker=null) ->


Successively removing and then adding a collaborator works.

--- app/assets/javascripts/controllers/overlays/show_collaborators_overlay_controller.js.coffee
+++ app/assets/javascripts/controllers/overlays/show_collaborators_overlay_controller.js.coffee
@@ -6,7 +6,7 @@
   availableCollaborators: Ember.computed.setDiff('allUsers', 'collaborators')

   addedCollaborations: Ember.computed.setDiff('collaborations.content','initialCollaborations')
-  removedCollaborations: Ember.computed.setDiff('initialCollaborations','collaborations')
+  removedCollaborations: Ember.computed.setDiff('initialCollaborations','collaborations.content')

   paper: null
   initialCollaborations: null
@@ -18,8 +18,23 @@

   actions:
     addNewCollaborator: (newCollaborator) ->
-      newCollaboration = @store.createRecord('collaboration', paper: @get('paper'), user: newCollaborator)
+      paper = @get('paper')
+      existingRecord = @store.all('collaboration').find (c) ->
+        # if this collaborator's record was previously removed from the paper make sure we use THAT one and not a
+        # new record.
+        c.get('oldPaper') == paper && c.get('user') == newCollaborator
+
+      newCollaboration = existingRecord || @store.createRecord('collaboration', paper: paper, user: newCollaborator)
       @get('collaborations').addObject(newCollaboration)
+
+    removeCollaborator: (collaborator) ->
+      collaboration = @get('collaborations').findBy('user', collaborator)
+      # since the relationship between paper and collaboration is a proper hasMany, if we remove the
+      # collaboration from the papers' collection of them ember will also unset the paper field on the collaboration.
+      # if the user tries to re-add that collaborator to the paper without reloading we need to do some extra checking
+      # to make sure that ember doesn't create a new record but rather uses the one we just removed here.
+      collaboration.set('oldPaper', collaboration.get('paper'))
+      @get('collaborations').removeObject(collaboration)

     save: ->
       addPromises = @get('addedCollaborations').map (collaboration) =>


fix loop?

fix loop?


remove styles from pasted nodes

remove styles from pasted nodes

--- src/paste-manager.coffee
+++ src/paste-manager.coffee
@@ -27,11 +27,19 @@
           _.each(extNodes, (node) ->
             node.parentNode.removeChild(node) if node.parentNode?
           )
+          removeStyles = (walker) ->
+            walker.firstChild()
+            walker.currentNode.removeAttribute('style')
+            while walker.nextNode()
+              walker.currentNode.removeAttribute('style')
+          walker = @container.ownerDocument.createTreeWalker(@container, NodeFilter.SHOW_ELEMENT, null, false)
+          removeStyles(walker)
           console.log @container.innerHTML
       )
     )

+
 window.Tandem ||= {}
 window.Tandem.PasteManager = TandemPasteManager


Use iconified button

Use iconified button

--- src/plugins/image.coffee
+++ src/plugins/image.coffee
@@ -43,6 +43,7 @@
                 close: (ev, ui) ->
                     jQuery('.image_button').removeClass('ui-state-clicked')
             dialog: null
+            buttonCssClass: null

         _create: ->
             widget = this
@@ -69,8 +70,18 @@
             buttonset = jQuery ""

             id = "#{@options.uuid}-image"
-            buttonset.append jQuery("<input id=\"#{id}\" type=\"checkbox\" />Image").button()
-            button = jQuery "##{id}", buttonset
+            buttonHolder = jQuery ''
+            buttonHolder.hallobutton
+                label: 'Images'
+                icon: 'icon-picture'
+                editable: @options.editable
+                command: null
+                queryState: false
+                uuid: @options.uuid
+                cssClass: @options.buttonCssClass
+            buttonset.append buttonHolder
+
+            button = buttonHolder.button();
             button.bind "change", (event) ->
                 if widget.options.dialog.dialog "isOpen"
                     widget._closeDialog()


Fix behavior of tab for Slava + fix separator

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -375,6 +375,7 @@
                                 @current_highlighted_suggestion = 0

                         if @current_suggestions[@current_highlighted_suggestion]?
+                            @show_suggestion()
                             @highlight_suggestion @current_highlighted_suggestion # Highlight the current suggestion
                             @write_suggestion
                                 suggestion_to_write: @current_suggestions[@current_highlighted_suggestion] # Auto complete with the highlighted suggestion
@@ -515,7 +516,7 @@
                 #to_describe: undefined

             result_non_white_char_after_cursor = @regex.get_first_non_white_char.exec(query_after_cursor)
-            if result_non_white_char_after_cursor isnt null and (result_non_white_char_after_cursor[1]?[0] isnt '.' and result_non_white_char_after_cursor[1]?[0] isnt '}' and result_non_white_char_after_cursor[1]?[0] isnt ')' and result_non_white_char_after_cursor[1]?[0] isnt ',')
+            if result_non_white_char_after_cursor isnt null and (result_non_white_char_after_cursor[1]?[0] isnt '.' and result_non_white_char_after_cursor[1]?[0] isnt '}' and result_non_white_char_after_cursor[1]?[0] isnt ')' and result_non_white_char_after_cursor[1]?[0] isnt ','  and result_non_white_char_after_cursor[1]?[0] isnt ';')
                 ###
                 and result_non_white_char_after_cursor[1]?[0] isnt '\''
                 and result_non_white_char_after_cursor[1]?[0] isnt '"')


Properly handle r and r()

Properly handle r and r()

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -78,7 +78,7 @@
                         suggestions[parent] = []
                     suggestions[parent].push full_tag #+something

-                    @map_state[tag] = command['returns']
+                    @map_state[full_tag] = command['returns'] # We use full_tag because we need to differentiate between r. and r(

             # Deep copy of suggestions
             for group of suggestions
@@ -448,18 +448,19 @@

             # Get the last completed function for description and suggestion
             last_function = @extract_last_function(query)
-            # Hack in case a new parenthesis is opened
-            if last_function is ''
+            just_description = false # Boolean to know if we want to display just the description and not the suggestions
+            # Hack in case a new parenthesis is opened. Ex: r.table('foo').filter( // We want to describe filter, not suggest r(
+            if /\s*/.test(query) is true # We have an empty cursor
                 last_function_full = @extract_last_function_for_description(query_before_cursor)
                 if last_function_full isnt ''
                     last_function = last_function_full
+                    just_description = true

             # Hack because last_function returns 'r' if the query is 'r'. and r isn't a function
             if last_function is query and last_function is 'r'
-                last_function = null
-
+                last_function = ''
             if @map_state[last_function]? and @suggestions[@map_state[last_function]]?
-                if not @suggestions[@map_state[last_function]]? or @suggestions[@map_state[last_function]].length is 0
+                if not @suggestions[@map_state[last_function]]? or @suggestions[@map_state[last_function]].length is 0 or just_description is true
                     @hide_suggestion()
                     last_function_for_description = @extract_last_function_for_description(query_before_cursor)

@@ -523,10 +524,15 @@
                             is_string = false
             return is_string

-        # Extract the last function of the current line, taking in account if we are in a string
+        # Extract the last completed function of the current line, taking in account if we are in a string
+        # Ex: r.d => r
+        # Ex: r.db('test').table( => db(
+        # Ex: r.db( => r
         extract_last_function: (query) =>
-            start = 0
             count_dot = 0
+            first_dot_position = 0
+            second_dot_position = query.length
+
             num_not_open_parenthesis = 0

             is_string = false
@@ -542,8 +548,10 @@
                         num_not_open_parenthesis++
                     else if query[i] is '.' and num_not_open_parenthesis <= 0
                         count_dot++
-                        if count_dot is 2
-                            start = i+1
+                        if count_dot is 1
+                            second_dot_position = i
+                        else if count_dot is 2
+                            first_dot_position = i+1
                             break
                 else if is_string is true
                     if query[i] is char_used
@@ -552,14 +560,22 @@
                         else
                             is_string = false

-            dot_position = query.indexOf('.', start)
-            dot_position = query.length if dot_position is -1
-            parenthesis_position = query.indexOf('(', start)
-            parenthesis_position = query.length if parenthesis_position is -1
-
-            end = Math.min dot_position, parenthesis_position
-
-            return query.slice(start, end).replace(/\s/g, '')
+
+            parenthesis_position = query.indexOf('(', first_dot_position+1)
+            if parenthesis_position is -1
+                parenthesis_position = query.length
+            else
+                parenthesis_position++
+
+            if parenthesis_position is 0
+                end = second_dot_position
+            else if parenthesis_position < second_dot_position
+                end = parenthesis_position
+            else
+                end = second_dot_position
+
+
+            return query.slice(first_dot_position, end).replace(/\s/g, '')

@@ -604,7 +620,10 @@
             if splitdata.length>1
                 for i in [0..splitdata.length-2]
                     @current_completed_query += splitdata[i]+'.'
-            element_currently_written = splitdata[splitdata.length-1]
+            if splitdata.length > 1
+                element_currently_written = splitdata[splitdata.length-1]
+            else
+                element_currently_written = query

             for char in @unsafe_to_safe_regexstr
@@ -614,8 +633,6 @@
             @current_suggestions = []
             for suggestion, i in suggestions
                 if pattern.test(suggestion)
-                    if splitdata[splitdata.length-1] is suggestion
-                        continue # Skip the case when we have an exact match including parenthesis
                     found_suggestion = true
                     @current_suggestions.push suggestion
                     @.$('.suggestion_name_list').append @template_suggestion_name
@@ -781,7 +798,7 @@
                 @.$('.loading_query_img').css 'display', 'none'
                 @results_view.render_error(@query, err)

-        # Separate the queries so we can execute them in a synchronous order. We use .run()\s{*}; to separate queries (and we make sure that the separator is not in a string)
+        # Separate the queries so we can execute them in a synchronous order. We use .run()\s*; to separate queries (and we make sure that the separator is not in a string)
         separate_queries: (query) =>
             start = 0
             count_dot = 0


teams: name made optional on private message creation

--- client/activity/lib/components/createprivatechannelmodal/index.coffee
+++ client/activity/lib/components/createprivatechannelmodal/index.coffee
@@ -121,9 +121,14 @@

     pattern =  /^[a-z0-9]+$/i

+    unless value
+      @setState invalidName: no
+      return yes
+
     if value and pattern.test value
       @setState invalidName: no
       return yes
+
     else
       @setState invalidName: yes
       return no
@@ -325,7 +330,10 @@
           {@renderAddParticipantInput()}

-          Name
+
+            Name
+             (optional)
+
           <input className='Reactivity-input' value={@state.name} maxlength='20' onChange={@bound 'setName'} onKeyDown={@bound 'onInputKeydown'}/>

             This is how this thread is going to appear on your sidebar.


fixing issues with disappearing flights

fixing issues with disappearing flights

--- src/scripts/brain.coffee
+++ src/scripts/brain.coffee
@@ -16,19 +16,18 @@
     @movePlane()

   movePlane: ->
-    tryInABit = =>
-      @timeout = setTimeout @movePlane.bind( @ )
+    return unless plane   = _.sample @myStationaryPlanes()
+    return unless airport = @bestDestinationFor plane

-    return tryInABit() unless plane   = _.sample @myStationaryPlanes()
-    return tryInABit() unless airport = @bestDestinationFor plane
-
-    clearTimeout @timeout if @timeout
     plane.location = "#{plane.location}->#{airport.name}"
     MessageBus.publish 'dataChange'

   bestDestinationFor: (plane) ->
     reachableAirportIn = (list) ->
-      airports = _.filter list, (airport) -> airport.key isnt plane.location
+      airports = _.filter list, (airport) ->
+        routeKey = "#{plane.location}->#{airport.key}"
+        !!Route.find(key: routeKey)
+
       _.sample airports

     reachableAirportIn( @otherAirports() ) or reachableAirportIn( @myAirports() )


Added small debug statement to test body deconstruction

--- src/scripts/hound.coffee
+++ src/scripts/hound.coffee
@@ -140,6 +140,7 @@
     body = req.body
     if body.token is process.env.SLASH_HOUND_TOKEN
       comps = body.text || []
+      console.log comps
       scope = comps[0] || 'self'
       if scope is Organization.name
         scope = 'org'


Removed None at commands

Removed None at commands

--- src/scripts/steam.coffee
+++ src/scripts/steam.coffee
@@ -11,7 +11,6 @@
 #
 # Commands:
 #   hubot daily deal - <It will show you Steam's daily deal>
-#   None
 #
 # Notes:
 #   soupselect depends on htmlparser so install both using "npm install soupselect" and "npm install htmlparser". Don't forget: "npm install validator"


fix hubot command prompt on help to be consistent with others

--- src/scripts/stock.coffee
+++ src/scripts/stock.coffee
@@ -8,7 +8,7 @@
 #   None
 #
 # Commands:
-#   stock <info|quote|price> for <ticker> - Get a stock price
+#   hubot stock <info|quote|price> for <ticker> - Get a stock price
 #
 # Author:
 #   eliperkins


add panel creates new icon with new id

add panel creates new icon with new id

--- app/assets/javascripts/panels.js.coffee
+++ app/assets/javascripts/panels.js.coffee
@@ -20,29 +20,37 @@
 #nested forms without link_to_function (which has been deprecated)
   add_fields = (event) ->
     #clone fields
-    old_fieldset = $('fieldset.nested_fields').last()
-    new_fieldset = old_fieldset.clone()
+    last_fieldset = $('fieldset.nested_fields').last()
+    new_fieldset = last_fieldset.clone()
     count_nested_fieldsets = $('fieldset.nested_fields').length
-    #modify input ids
+    #modify ids
     new_fieldset.children().each ->
-      form_tag = $(this)
-      if form_tag.attr('for')
-        old_for = form_tag.attr('for')
+      element = $(this)
+
+      if element.attr('for')
+        old_for = element.attr('for')
         new_for = old_for.replace(new RegExp(/[0-9]+/), "#{count_nested_fieldsets}")
-        form_tag.attr('for', new_for)
-      if form_tag.attr('id')
-        old_id = form_tag.attr('id')
+        element.attr('for', new_for)
+      if element.attr('id')
+        old_id = element.attr('id')
         new_id = old_id.replace(new RegExp(/[0-9]+/), "#{count_nested_fieldsets}")
-        form_tag.attr('id', new_id)
-      if form_tag.attr('name')
-        old_name = form_tag.attr('name')
+        element.attr('id', new_id)
+      if element.attr('name')
+        old_name = element.attr('name')
         new_name = old_name.replace(new RegExp(/[0-9]+/), "#{count_nested_fieldsets}")
-        form_tag.attr('name', new_name)
+        element.attr('name', new_name)
+      #required for icon to work. This must go at end of each loop (so it does
+      #not get overwritten by above code)
+      if $(this).is('#icon1')
+        last_icon_id_number = parseInt(last_fieldset.children('.icon').attr('id').slice(4))
+        old_id = element.attr('id')
+        new_id = old_id.replace(new RegExp(/[0-9]+/), "#{last_icon_id_number + 1}")
+        element.attr('id', new_id)
     new_fieldset.insertBefore($('.add_fields'))
     $('Remove panel').insertBefore($(event.target))

   remove_fields = () ->
-    alert('hello')
+    alert 'hello'
     #$(this).closest('.nested_fields').css('border', '1px solid red')

   $(document).on 'click', '.add_fields', (event) ->


sync fences pattern regex to redoc

sync fences pattern regex to redoc

--- app/assets/javascripts/task_list.coffee
+++ app/assets/javascripts/task_list.coffee
@@ -136,8 +136,8 @@

 # Used to skip checkbox markup inside of code fences.
 # http://rubular.com/r/TfCDNsy8x4
-startFencesPattern = /^\s*`{3}.*$/
-endFencesPattern = /^\s*`{3}$/
+startFencesPattern = /^\s{0,3}((```+)|(~~~+))(\S*\s*)$/
+endFencesPattern = /^\s{0,3}((```+)|(~~~+))$/

 # Used to filter out potential mismatches (items not in lists).
 # http://rubular.com/r/OInl6CiePy


Fix missing injection of analytics service on related tasks directive.

--- app/coffee/modules/related-tasks.coffee
+++ app/coffee/modules/related-tasks.coffee
@@ -25,7 +25,7 @@

 module = angular.module("taigaRelatedTasks", [])

-RelatedTaskRowDirective = ($repo, $compile, $confirm, $rootscope, $loading, $analytics) ->
+RelatedTaskRowDirective = ($repo, $compile, $confirm, $rootscope, $loading) ->
     templateView = _.template("""

@@ -256,7 +256,7 @@
             $el.off()

     return {link: link}
-module.directive("tgRelatedTaskCreateForm", ["$tgRepo", "$compile", "$tgConfirm", "$tgModel", "$tgLoading", RelatedTaskCreateFormDirective])
+module.directive("tgRelatedTaskCreateForm", ["$tgRepo", "$compile", "$tgConfirm", "$tgModel", "$tgLoading", "$tgAnalytics", RelatedTaskCreateFormDirective])

 RelatedTaskCreateButtonDirective = ($repo, $compile, $confirm, $tgmodel) ->
     template = _.template("""


hub/email: ignore cc/bcc empty strings. maybe related to #2507

--- src/smc-hub/email.coffee
+++ src/smc-hub/email.coffee
@@ -121,9 +121,9 @@
             personalization = new helper.Personalization()
             personalization.setSubject(opts.subject)
             personalization.addTo(to_email)
-            if opts.cc
+            if opts.cc?.length > 0
                 personalization.addCc(new helper.Email(opts.cc))
-            if opts.bcc
+            if opts.bcc?.length > 0
                 personalization.addBcc(new helper.Email(opts.bcc))

             # one or more strings to categorize the sent emails on sendgrid


Polygon tool cancels if you undo/redo

Polygon tool cancels if you undo/redo

--- src/tools/Polygon.coffee
+++ src/tools/Polygon.coffee
@@ -41,6 +41,8 @@
       lc.setShapesInProgress(@_getShapes(lc))
       lc.repaintLayer('main')

+    unsubscribeFuncs.push lc.on 'undo', => @_cancel(lc)
+    unsubscribeFuncs.push lc.on 'redo', => @_cancel(lc)
     unsubscribeFuncs.push lc.on 'lc-pointerdown', onDown
     unsubscribeFuncs.push lc.on 'lc-pointerdrag', onMove
     unsubscribeFuncs.push lc.on 'lc-pointermove', onMove


Redo Node.js detection

Redo Node.js detection

--- src/tools/remotes.coffee
+++ src/tools/remotes.coffee
@@ -3,11 +3,10 @@
 ###

 module.exports =
-exports = (force)->
+list = ->
   r = []
   for k, v of dists
-    f = fs.BuildPath cache, "#{k}.tsv"
-    if not force and cached f
+    if cached f = dpath k
       tab = fs.OpenTextFile f
       .ReadAll()
     else
@@ -19,26 +18,31 @@
     tab = for z in tsv tab when msi z
       new Remote z, k
     r = r.concat tab
-  store r.sort semver.cmpi
+  r.sort semver.cmpi
+
+dpath = (dist)->
+  fs.BuildPath cache, "#{dist}.tsv"

 cached = (f)->
   fs.FileExists(f) and
   new Date - fs.GetFile(f).DateLastModified < 1000*60*60*24

+cachedAll = ->
+  for k, v of dists when not cached dpath k
+    return
+  true
+
 msi = (line)->
   ~line.files.indexOf '-msi'

-latest = 0
-store = (list)->
-  if list and list.length
-    latest = list[list.length - 1]
-  list
-
 # Check for upgrade
-exports.$ = ->
-  return unless latest
-  return if local = latest.local '*'
-  latest.$[0].join '.'
+list.$ = ->
+  return unless cachedAll()
+  remote = list()
+  return unless remote and remote.length
+  remote = remote[remote.length - 1]
+  return if remote.local '*'
+  remote.$[0].join '.'

 Remote = (line, dist)->
   semver = for z in line.version.split /\D+/ when z.length


DRY up, mostly by using async.detect

DRY up, mostly by using async.detect

--- check_licences_in_github.coffee
+++ check_licences_in_github.coffee
@@ -1,37 +1,52 @@
 #!/usr/bin/env coffee
+# check_licences_in_github.coffee
+# Script to check the existence of licences in ScraperWiki's
+# github repos.
+# Run from command line.

 request = require 'request'
 async = require 'async'
+
 compliant = 0
 total = 0
+
+hub = "https://raw.github.com"
+ProperOrg = "ScraperWiki"
+org = ProperOrg.toLowerCase()

 print = (status, name) ->
   prefixes = ["\x1b[31m ✗ ", "\x1b[32m ✓ ", "\x1b[33m ✓ "]
   console.log prefixes[status] + name + "\x1b[0m"
   if status > 0
-    compliant = compliant + 1
+    compliant += 1

 check_licence = (repo, callback) ->
-  request.get "https://raw.github.com/scraperwiki/" + repo.name + '/master/LICENCE', (err, resp, body) ->
-    if resp.statusCode == 200
-      print(1, repo.name)
-      callback(null, 1)
-    else
-      request.get "https://raw.github.com/scraperwiki/" + repo.name + '/master/LICENSE', (err, resp, body) ->
-        if resp.statusCode == 200
-          print(2, repo.name + ' (LICENSE)')
-          callback(null, 1)
+  check_single = (fname, cb) ->
+    # (Intended to be used in async.detect)
+    # *fname* is the final part of the pathname to
+    # check the existence of.
+    # *cb* will be called with false if the file does not exist;
+    # true otherwise.
+    request.get "#{hub}/#{org}/#{repo.name}/master/#{fname}",
+      (err, resp) -> cb(resp.statusCode == 200)
+  async.detectSeries ['LICENCE', 'LICENSE', 'LICENSE.txt'],
+    check_single,
+    (name) ->
+      if not name?
+        print 0, repo.name
+        callback null, 1
+      else
+        if name == 'LICENCE'
+          print 1, repo.name
         else
-          request.get "https://raw.github.com/scraperwiki/" + repo.name + '/master/LICENSE.txt', (err, resp, body) ->
-            if resp.statusCode == 200
-              print(2, repo.name + ' (LICENSE.txt)')
-            else
-              print(0, repo.name)
-            callback(null, 1)
+          print 2, "#{repo.name} (#{name})"
+        callback null, 1

-request.get "https://api.github.com/orgs/scraperwiki/repos", (err, resp, body) ->
-  repos = JSON.parse(body)
-  total = repos.length
-  console.log "Scanning licences for " + total + " repositories..."
-  async.map repos, check_licence, (err, results) ->
-    console.log "ScraperWiki is " + Math.round(compliant*100/total) + "% compliant"+request.get "https://api.github.com/orgs/#{org}/repos",
+  (err, resp, body) ->
+    repos = JSON.parse(body)
+    total = repos.length
+    console.log "Scanning licences for #{total} repositories..."
+    async.map repos, check_licence, ->
+      percentage = Math.round(compliant*100/total)
+      console.log "#{ProperOrg} is #{percentage}% compliant"


utils: use the new generalized utils.extend

--- client/app/MainApp/utils.coffee
+++ client/app/MainApp/utils.coffee
@@ -1,5 +1,4 @@
-
-__utils.extend
+__utils.extend __utils,

   getPaymentMethodTitle: (billing)->
     # for convenience, accept either the payment method, or the billing object


Collab: check if mywatchmap exists

Collab: check if mywatchmap exists

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -1576,7 +1576,7 @@
   getCollaborationData: (callback = noop) =>

     collaborationData =
-      watchMap        : @myWatchMap.values()
+      watchMap        : @myWatchMap?.values()
       amIHost         : @amIHost

     callback collaborationData


IDE: Create fake filetree when machine is turned off.

--- client/ide/AppController.coffee
+++ client/ide/AppController.coffee
@@ -214,12 +214,20 @@
         return unless machine

         if state in [ 'Stopped', 'NotInitialized', 'Terminated', 'Starting', 'Building' ]
+          nickname     = KD.nick()
+          machineLabel = machine.label
+          splashs      = IDE.splashMarkups
+
           tabView      = @activeTabView
-          terminalView = new KDCustomHTMLView partial: IDE.terminalSplashMarkup
+          terminalView = new KDCustomHTMLView partial: splashs.getTerminal nickname
           terminalPane = tabView.parent.createPane_ terminalView, { name: 'Terminal' }
+
+          finderView   = new KDCustomHTMLView partial: splashs.getFileTree nickname, machineLabel
+          @workspace.panel.getPaneByName('filesPane').finderPane.addSubView finderView, '.nfinder .jtreeview-wrapper'

           KD.utils.defer =>
             @machineStateModal.once 'IDEBecameReady', =>
+              finderView.destroy()
               tabView.removePane_ terminalPane
               @createNewTerminal @machineStateModal.getData()
               @setActiveTabView @ideViews.first.tabView


Tests: IDE: File: Fixed jumpToFile selector.

--- client/test/lib/ide/file.coffee
+++ client/test/lib/ide/file.coffee
@@ -196,7 +196,7 @@
     helpers.waitForVMRunning(browser)

     searchModalSelector  = '.file-finder'
-    findInFilesSelector  = '.kdlistview-contextmenu li.jump-to-file'
+    findInFilesSelector  = '.kdlistview-contextmenu li.find-file-by-name'
     paneSelector         = '.pane-wrapper .kdsplitview-panel .application-tab-handle-holder .pythonpy'

     ideHelpers.openNewFile(browser)
@@ -211,4 +211,4 @@
       .click                   searchModalSelector + ' .file-item:first-child'
       .waitForElementVisible   '.ws-tabview .kdtabview .pythonpy.active', 20000 # Assertion
       .waitForElementVisible   paneSelector, 20000 # Assertion
-      .end()+      .end()


Check for rendered HTML before rendering in client

--- client/views/voltagePage.coffee
+++ client/views/voltagePage.coffee
@@ -33,7 +33,7 @@
       page = Page.first name: slug

       if page
-        return marked page.text
+        return page.html || marked page.text

       ''
     markdown: () =>


jraphical: flatten control flow

jraphical: flatten control flow

--- node_modules_koding/jraphical/lib/module.coffee
+++ node_modules_koding/jraphical/lib/module.coffee
@@ -438,16 +438,15 @@
               $set: timestamp: new Date
             }, (err)->
               callback null, module
-          else
-            if target
-              addIt this, adder, target, callback
-            else
-              target = new def.targetType
-              target.save (err)=>
-                if err
-                  callback err
-                else
-                  addIt this, adder, target, callback
+          else if target
+            addIt this, adder, target, callback
+          else
+            target = new def.targetType
+            target.save (err)=>
+              if err
+                callback err
+              else
+                addIt this, adder, target, callback

   remove_ = @::remove
   remove:(callback=->)->


fix a typo

fix a typo

--- lib/Components/StepButton.coffee
+++ lib/Components/StepButton.coffee
@@ -41,7 +41,7 @@
     h 'div', {
       'ev-click': hg.send channels.next
       'className': BTN_ICON_MAP[state.type()]
-      'distabled': !state.waiting
+      'disabled': !state.waiting
     }, [
     ]


Add *.es and *.es6 support (Fix for bug #41)

--- src/tokenizer/TokenizerFactory.coffee
+++ src/tokenizer/TokenizerFactory.coffee
@@ -7,7 +7,7 @@
   tokenizers: {}

   LANGUAGES:
-    javascript: ['js']
+    javascript: ['js', 'es', 'es6']
     typescript: ['ts']
     jsx: ['jsx']
     haxe: ['hx', 'hxml']


Don't parse zero width spaces

Don't parse zero width spaces

--- src/trix/utilities/html_parser.coffee
+++ src/trix/utilities/html_parser.coffee
@@ -57,8 +57,9 @@
       @appendBlockForAttributes({})

   processTextNode: (node) ->
-    string = node.textContent.replace(/\s/, " ")
-    @appendStringWithAttributes(string, getAttributes(node.parentNode))
+    unless node.textContent is Trix.ZERO_WIDTH_SPACE
+      string = node.textContent.replace(/\s/, " ")
+      @appendStringWithAttributes(string, getAttributes(node.parentNode))

   processElementNode: (node) ->
     switch node.tagName.toLowerCase()


Show progress bar immediately on restore if no cache exists

--- src/turbolinks/browser_adapter.coffee
+++ src/turbolinks/browser_adapter.coffee
@@ -15,8 +15,12 @@
     visit.restoreSnapshot()

   visitRequestStarted: (visit) ->
-    @showProgressBarAfterDelay() unless visit.snapshotRestored
     @progressBar.setValue(0)
+    unless visit.snapshotRestored
+      if visit.hasSnapshot() or visit.action isnt "restore"
+        @showProgressBarAfterDelay()
+      else
+        @showProgressBar()

   visitRequestProgressed: (visit) ->
     @progressBar.setValue(visit.progress)


Add md5 to file delete operation

Add md5 to file delete operation

--- lib/layervault/nodes/file.coffee
+++ lib/layervault/nodes/file.coffee
@@ -1,5 +1,6 @@
 Node = require '../node'
 UploadService = require '../services/upload'
+Md5Service = require '../services/md5'

 Revision = require './revision'

@@ -13,7 +14,17 @@
     service = new UploadService(@, options)
     service.perform cb.bind(@)

-  delete: (cb) -> @api.delete @nodePath, {}, cb.bind(@)
+  delete: (opts, cb) ->
+    if opts.md5?
+      md5 = opts.md5
+    else if opts.localPath?
+      service = new Md5Service(opts.localPath)
+      service.calculate (md5) => @delete {md5: md5}, cb
+    else
+      throw "Deleting a file requires the md5 of the latest revision"
+
+    @api.delete @nodePath, {md5: md5}, cb.bind(@)
+
   move: (to, cb) -> @api.post("#{@nodePath}/move", { to: to }, cb.bind(@))
   rename: @move


Use atom.commands to subscribe to view-local commands

--- lib/markdown-preview-view.coffee
+++ lib/markdown-preview-view.coffee
@@ -69,25 +69,27 @@
     null

   handleEvents: ->
-    @subscribe atom.syntax, 'grammar-added grammar-updated', _.debounce((=> @renderMarkdown()), 250)
-    @subscribe this, 'core:move-up', => @scrollUp()
-    @subscribe this, 'core:move-down', => @scrollDown()
-    @subscribe this, 'core:save-as', =>
-      @saveAs()
-      false
-    @subscribe this, 'core:copy', =>
-      return false if @copyToClipboard()
-
-    @subscribeToCommand atom.workspaceView, 'markdown-preview:zoom-in', =>
-      zoomLevel = parseFloat(@css('zoom')) or 1
-      @css('zoom', zoomLevel + .1)
-
-    @subscribeToCommand atom.workspaceView, 'markdown-preview:zoom-out', =>
-      zoomLevel = parseFloat(@css('zoom')) or 1
-      @css('zoom', zoomLevel - .1)
-
-    @subscribeToCommand atom.workspaceView, 'markdown-preview:reset-zoom', =>
-      @css('zoom', 1)
+    @subscribe atom.grammars.onDidAddGrammar _.debounce((=> @renderMarkdown()), 250)
+    @subscribe atom.grammars.onDidUpdateGrammar _.debounce((=> @renderMarkdown()), 250)
+
+    atom.commands.add @element,
+      'core:move-up': =>
+        @scrollUp()
+      'core:move-down': =>
+        @scrollDown()
+      'core:save-as': (event) =>
+        event.stopPropagation()
+        @saveAs()
+      'core:copy': (event) =>
+        event.stopPropagation() if @copyToClipboard()
+      'markdown-preview:zoom-in': =>
+        zoomLevel = parseFloat(@css('zoom')) or 1
+        @css('zoom', zoomLevel + .1)
+      'markdown-preview:zoom-out': =>
+        zoomLevel = parseFloat(@css('zoom')) or 1
+        @css('zoom', zoomLevel - .1)
+      'markdown-preview:reset-zoom': =>
+        @css('zoom', 1)

     changeHandler = =>
       @renderMarkdown()


remove unnecessary repetition

remove unnecessary repetition

--- lib/motions/search-motion.coffee
+++ lib/motions/search-motion.coffee
@@ -75,8 +75,6 @@
   constructor: (@editor, @vimState) ->
     super(@editor, @vimState)
     @viewModel = new SearchViewModel(@)
-    Search.currentSearch = @
-    @reverse = @initiallyReversed = false

   compose: (input) ->
     super(input)
@@ -87,8 +85,6 @@

   constructor: (@editor, @vimState) ->
     super(@editor, @vimState)
-    Search.currentSearch = @
-    @reverse = @initiallyReversed = false

     # FIXME: This must depend on the current language
     defaultIsKeyword = "[@a-zA-Z0-9_\-]+"
@@ -142,8 +138,6 @@

   constructor: (@editor, @vimState) ->
     super(@editor, @vimState)
-    Search.currentSearch = @
-    @reverse = @initiallyReversed = false

   isComplete: -> true


Response debugging

Response debugging

--- lib/receivers/tcpReceiver.coffee
+++ lib/receivers/tcpReceiver.coffee
@@ -94,10 +94,14 @@
             console.log "About to respond"
             # processBL returns a pre-assembled response object
             if err?
+              console.log "Responding with error"
+              console.log err
               socket.write JSON.stringify composeErrorReply('Internal Error', 'Unable to run dlc script', err)
               socket.write '\n'
               return
             else
+              console.log "Responding with success"
+              console.log result
               # make sure result is present, undefined is not stringified into the json
               if result is undefined then result = null


use window.document.title instead of setDocumentTitle()

--- client/code/view/subnav.coffee
+++ client/code/view/subnav.coffee
@@ -1,15 +1,8 @@
-setDocumentTitle = (text) ->
-  if typeof text is 'string'
-    window.document.title = text
-  else
-    window.document.title = 'ScraperWiki'
-
-
 class Cu.View.Subnav extends Backbone.View
   className: 'subnav-wrapper'

   render: ->
-    setDocumentTitle @options.title or 'ScraperWiki'
+    window.document.title = @options.title or 'ScraperWiki'
     @$el.html JST['subnav'] @options
     @

@@ -27,7 +20,7 @@

   render: ->
     name = window.user.effective.displayName or window.user.effective.shortName
-    setDocumentTitle "#{name}’s data hub | ScraperWiki"
+    window.document.title = "#{name}’s data hub | ScraperWiki"
     h1 = """

           #{name}’s data hub
@@ -222,7 +215,7 @@
     'keyup #editable-input input': 'keypressOnEditableName'

   initialize: ->
-    setDocumentTitle "#{@model.get 'displayName'} | ScraperWiki"
+    window.document.title = "#{@model.get 'displayName'} | ScraperWiki"
     @toolsView = new Cu.View.DatasetTools
       model: @model
       view: @options.view
@@ -252,7 +245,7 @@

   renderName: ->
     name = @model.get 'displayName'
-    setDocumentTitle "#{name} | ScraperWiki"
+    window.document.title = "#{name} | ScraperWiki"
     @$el.find('#dataset-meta h3').text name
     @$el.find('#dataset-meta input').val name

@@ -332,7 +325,7 @@
     plan = @options.plan # this should be passed in by router/main.coffee
     plan = plan.toUpperCase()[0] + plan.toLowerCase()[1..]
     @$el.html JST['signupnav'] plan: plan
-    setDocumentTitle "#{plan} | Sign Up | ScraperWiki"
+    window.document.title = "#{plan} | Sign Up | ScraperWiki"
     this

@@ -340,7 +333,7 @@
   className: 'subnav-wrapper'

   render: ->
-    setDocumentTitle @options.title
+    window.document.title = @options.title or 'ScraperWiki'
     @$el.html JST['helpnav'] @options
     this

@@ -350,7 +343,7 @@
   className: 'subnav-wrapper'

   render: ->
-    setDocumentTitle "#{@options.name} | ScraperWiki"
+    window.document.title = "#{@options.name} | ScraperWiki"
     @$el.html("""


Separate currency with commas

Separate currency with commas

--- client/lanes/lib/format.coffee
+++ client/lanes/lib/format.coffee
@@ -8,4 +8,4 @@

 Lanes.u.format.currency = (v) ->
     v ||= '0'
-    _.sprintf('%0.2f', parseFloat(v))
+    _.sprintf('%0.2f', parseFloat(v)).replace(/\B(?=(\d{3})+\b)/g, ",")


blank path segments should be considered falsy

--- resources/assets/coffee/_classes/beatmap-discussion-helper.coffee
+++ resources/assets/coffee/_classes/beatmap-discussion-helper.coffee
@@ -154,8 +154,9 @@
     ret =
       beatmapsetId: if isFinite(beatmapsetId) then beatmapsetId
       beatmapId: if isFinite(beatmapId) then beatmapId
-      mode: mode ? @DEFAULT_MODE
-      filter: filter ? @DEFAULT_FILTER
+      # empty path segments are ''
+      mode: mode || @DEFAULT_MODE
+      filter: filter || @DEFAULT_FILTER

     if url.hash[1] == '/'
       discussionId = parseInt(url.hash[2..], 10)


It is now class. And works with persisted mount

--- resources/assets/coffee/react/_components/beatmapset-panel.coffee
+++ resources/assets/coffee/react/_components/beatmapset-panel.coffee
@@ -19,25 +19,30 @@
 {div,a,i,span} = React.DOM
 el = React.createElement

-@BeatmapsetPanel = React.createClass
-  getInitialState: ->
-    preview: 'ended'
-    previewDuration: 0
+class @BeatmapsetPanel extends React.PureComponent
+  constructor: (props) ->
+    super props
+
+    @eventId = "beatmapsetPanel-#{props.beatmap.beatmapset_id}-#{osu.generateId()}"
+
+    @state =
+      preview: 'ended'
+      previewDuration: 0

-  componentDidMount: ->
-    @eventId = "beatmapsetPanel#{@props.beatmap.beatmapset_id}"
-
+  componentDidMount: =>
     $.subscribe "osuAudio:initializing.#{@eventId}", @previewInitializing
     $.subscribe "osuAudio:playing.#{@eventId}", @previewStart
     $.subscribe "osuAudio:ended.#{@eventId}", @previewStop
+    $(document).on "turbolinks:before-cache.#{@eventId}", @componentWillUnmount

-  componentWillUnmount: ->
+  componentWillUnmount: =>
     $.unsubscribe ".#{@eventId}"
+    $(document).off ".#{@eventId}"

-  render: ->
+  render: =>
     # this is actually "beatmapset"
     beatmapset = @props.beatmap

@@ -115,7 +120,7 @@
       div className: 'beatmapset-panel__shadow'

-  previewInitializing: (_e, {url, player}) ->
+  previewInitializing: (_e, {url, player}) =>
     if url != @props.beatmap.previewUrl
       return @previewStop()

@@ -124,7 +129,7 @@
       previewDuration: 0

-  previewStart: (_e, {url, player}) ->
+  previewStart: (_e, {url, player}) =>
     if url != @props.beatmap.previewUrl
       return @previewStop()

@@ -133,7 +138,7 @@
       previewDuration: player.duration

-  previewStop: ->
+  previewStop: =>
     return if @state.preview == 'ended'

     @setState


hub/kucalc: expose total idle timeout as part of the quota -- used for scheduling on nodes

--- src/smc-hub/kucalc/quota.coffee
+++ src/smc-hub/kucalc/quota.coffee
@@ -11,6 +11,9 @@
 MIN_POSSIBLE_MEMORY =
     member    : 300
     nonmember : 200
+
+{DEFAULT_QUOTAS} = require('smc-util/upgrade-spec')
+

 exports.quota = (settings, users) ->
     # so can assume defined below
@@ -26,6 +29,7 @@
         cpu_limit      : 1           # upper bound on vCPU's
         cpu_request    : 0           # will hold guaranteed min number of vCPU's as a float from 0 to infinity.
         privileged     : false       # for elevated docker privileges (FUSE mounting, later more)
+        idle_timeout   : DEFAULT_QUOTAS.mintime

     # network access
     if settings.network  # free admin-set
@@ -66,6 +70,12 @@
         quota.memory_limit = to_int(settings.memory)
     for _, val of users
         quota.memory_limit += to_int(val?.upgrades?.memory)
+
+    # idle timeout: not used for setting up the project quotas, but necessary to know for precise scheduling on nodes
+    if settings.mintime
+        quota.idle_timeout = to_int(settings.mintime)
+    for _, val of users
+        quota.idle_timeout += to_int(val?.upgrades?.mintime)

     # memory request
     if settings.memory_request


refactor code for readability

refactor code for readability

--- app/scripts/components/Bullet.js.coffee
+++ app/scripts/components/Bullet.js.coffee
@@ -1,19 +1,20 @@
 Crafty.c 'Bullet',
   init: ->
-    @requires '2D, Canvas, Color, Collision'
-
+    @addComponent '2D, Canvas, Color, Collision'
+
   fire: (properties) ->
-    @attr damage: properties.damage
-
-    @bind 'EnterFrame', ->
-      @x = @x + properties.speed
-
-      maxX = (-Crafty.viewport._x + (Crafty.viewport._width / Crafty.viewport._scale))
-      if (@x > maxX + 200)
+    @attr(damage: properties.damage).bind('EnterFrame', =>
+      @_nextPosition(properties.speed)
+      if @x > @_maxXforViewPort()
         # Maybe send a bullet miss event
         @destroy()
-
-    @onHit 'Edge', ->
+    ).onHit 'Edge', ->
       @destroy()
-
     this
+
+  _nextPosition: (speed) ->
+    @x = @x + speed
+
+  _maxXforViewPort: ->
+   maxX = -Crafty.viewport._x + Crafty.viewport._width / Crafty.viewport._scale
+   maxX + 200


refactor(HomeCtrl): define config file name outside of methods to reuse

--- app/scripts/controllers/HomeCtrl.coffee
+++ app/scripts/controllers/HomeCtrl.coffee
@@ -12,13 +12,13 @@
 		$scope._ = _ = require "lodash"

 		$scope.localPath = "D:/vk-music/"
+		configFilename = $scope.localPath + ".vk-sync.json"
 		$scope.rawRemoteList = []

 		$scope.localList = {}
 		$scope.remoteList = {}
 		saveListsToFile = _.throttle ->
 			console.error "saveListsToFile called, see stack"
-			configFilename = $scope.localPath + ".vk-sync.json"
 			content =
 				local: $scope.localList
 				remote: $scope.remoteList
@@ -29,7 +29,6 @@
 		, 1000

 		loadListsFromFile = ( callback ) ->
-			configFilename = $scope.localPath + ".vk-sync.json"
 			fs.readFile configFilename, ( err, data ) ->
 				try
 					content = JSON.parse data


Added full state names to FIPS_to_state hashtable for use in dropdown to come.


allow variable CPUs number

allow variable CPUs number

--- src/api/vm.coffee
+++ src/api/vm.coffee
@@ -1031,14 +1031,23 @@

   json = parseXml(body)
   # Find index of needed objects for getting their values after
-  # CPU: TODO fetch every CPUs!
-  cpuIndex = $findIndex(json.rrd.ds, 'name': 'cpu0')
+  # CPU: fetch every CPUs!
+  cpusIndexes = []
+  index = 0
+  current_cpu = 'cpu'+index
+  while $findIndex(json.rrd.ds, 'name': current_cpu) isnt -1
+    cpusIndexes.push(index)
+    index++
+    current_cpu = 'cpu'+index
+
   memoryFreeIndex = $findIndex(json.rrd.ds, 'name': 'memory_internal_free')
   memoryIndex = $findIndex(json.rrd.ds, 'name': 'memory')
   memoryFree = []
   memoryUsed = []
   memory = []
-  cpu = []
+  cpus = []
+  $forEach cpusIndexes, (value, key) ->
+    cpus[value] = []
   date = [] #TODO
   # TODO: fetch other info: network, IOPS etc.

@@ -1050,15 +1059,18 @@
     memoryFree.push(n.v[memoryFreeIndex]*1024)
     memoryUsed.push(Math.round(parseInt(n.v[memoryIndex])-(n.v[memoryFreeIndex]*1024)))
     memory.push(parseInt(n.v[memoryIndex]))
-    cpu.push(n.v[cpuIndex]*100)
     date.push(key)
+    # build the multi dimensional CPUs array
+    $forEach cpusIndexes, (value, key) ->
+      cpus[value].push(n.v[cpusIndexes[value]]*100)
+
     return

   result.memoryFree = memoryFree
   result.memoryUsed = memoryUsed
   result.memory = memory
   result.date = date
-  result.cpu = cpu
+  result.cpus = cpus

   return result


Fixed a typo in Batman.RestStorage::destroy signature.

--- src/batman.coffee
+++ src/batman.coffee
@@ -1831,7 +1831,7 @@
         success: (data) => callback(undefined, @getRecordsFromData(data))
         error: (err) -> callback(err)

-  destroy: (record, optiosn, callback) ->
+  destroy: (record, options, callback) ->
     @optionsForRecord record, true, (err, options) ->
       if err
         callback(err)


Batman.Property::isDead defaults to false

--- src/batman.coffee
+++ src/batman.coffee
@@ -439,6 +439,7 @@
   value: null
   sources: null
   isProperty: true
+  isDead: false
   eventClass: Batman.PropertyEvent

   isEqual: (other) ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/sprite.coffee
+++ src/sprite.coffee
@@ -56,7 +56,7 @@
     @param {Number} x Position on the x axis to draw the sprite
     @param {Number} y Position on the y axis to draw the sprite
     @param {Number} width Width to tile the
-    @param {Number} height
+    @param {Number} height Height to tile the
     ###
     fill: (canvas, x, y, width, height, repeat="repeat") ->
       pattern = canvas.createPattern(image, repeat)


adding loadByName method

adding loadByName method

--- src/sprite.coffee
+++ src/sprite.coffee
@@ -117,5 +117,20 @@
   ###
   window.Sprite.fromURL = Sprite.load

+  ###*
+  Loads a sprite with the given name.
+
+  @name loadByName
+  @methodOf Sprite
+
+  @param {String} name
+  @param [callback]
+
+  @type Sprite
+  ###
+  window.Sprite.loadByName = (name, callback) ->
+    url = "#{BASE_URL}/images/#{name}.png"
+    Sprite.load(url, callback)
+
 )()


Documentation

Documentation

--- src/sprite.coffee
+++ src/sprite.coffee
@@ -2,7 +2,7 @@
 The Sprite class provides a way to load images for use in games.

 By default, images are loaded asynchronously. A proxy object is
-returned immediately but though it has a draw method it will not
+returned immediately. Even though it has a draw method it will not
 draw anything to the screen until the image has been loaded.

 @name Sprite
@@ -30,9 +30,9 @@
     @name draw
     @methodOf Sprite#

-    @param canvas
-    @param x
-    @param y
+    @param {PowerCanvas} canvas Reference to the canvas to draw the sprite on
+    @param {Number} x Position on the x axis to draw the sprite
+    @param {Number} y Position on the y axis to draw the sprite
     ###
     draw: (canvas, x, y) ->
       canvas.drawImage(


solve systems of equations

solve systems of equations

--- src/system.coffee
+++ src/system.coffee
@@ -2,6 +2,101 @@
 # this work is subject to the terms of the MIT license

 define ['coffeequate'], (CQ) ->
+	class SolveError extends Error
+		constructor: (@message, @cause) ->
+			@stack = @cause?.stack
+
+	class OverdeterminedError extends SolveError
+		constructor: ->
+			super('overdetermined', null)
+
+	class UnderdeterminedError extends SolveError
+		constructor: ->
+			super('underdetermined', null)
+
+	class Expr
+		constructor: (@cq) ->
+			throw 'undefined expression' unless @cq?
+			@vars = @cq.getAllVariables?().sort() ? []
+
+		solve: (x) -> (new Expr(y) for y in @cq.solve(x))
+
+		sub: (x) ->
+			y = {}
+			for [k, v] in x
+				y[k] = v.cq
+			new Expr(@cq.sub(y))
+
+		approx: -> @cq.approx()
+
+		toString: -> @cq.toString()
+
+		has: (x) ->
+			for y in @vars
+				return true if x is y
+			false
+
+		@common: (lhs, rhs) ->
+			i = 0
+			j = 0
+			common = []
+			while i < lhs.vars.length and j < rhs.vars.length
+				x = lhs.vars[i]
+				y = rhs.vars[j]
+				if      x < y then i++
+				else if x > y then j++
+				else
+					common.push(x)
+					i++
+					j++
+			return common
+
+		common: (other) -> @constructor.common(this, other)
+
+	unique = (array) ->
+		return array.slice().sort().filter((y) ->
+			isUnique = y isnt @last
+			@last = y
+			return isUnique
+		, {last: null})
+
 	class System
+		constructor: -> @exprList = []

+		add: (s) -> @exprList.push(new Expr(CQ(s)))
+
+		vars: ->
+			vars = []
+			vars.push(expr.vars...) for expr in @exprList
+			return unique(vars)
+
+		@solve: (x, expr, exprList) ->
+			if expr.vars.length is 1 and expr.vars[0] is x
+				return (y.approx() for y in expr.solve(x))
+			results = []
+			for e0 in exprList
+				common = expr.common(e0)
+				continue unless common.length > 0
+				for y in common
+					continue if y is x
+					for e1 in e0.solve(y)
+						results.push(@solve(x,
+							expr.sub([[y, e1]]),
+							exprList.filter((e2) -> e2 isnt e0))...)
+			return results
+
+		solve: (x) ->
+			vars = @vars()
+			n = vars.length
+			throw new UnderdeterminedError() if @exprList.length < n
+			throw new OverdeterminedError()  if @exprList.length > n
+
+			results = []
+			for expr in @exprList
+				continue unless expr.has(x)
+				remaining = @exprList.filter((e) -> e isnt expr)
+				results.push(@constructor.solve(x, expr, remaining)...)
+			return unique(results)

+	System
+


Shim Buffer.prototype.toJSON

Shim Buffer.prototype.toJSON

--- src/bundle.coffee
+++ src/bundle.coffee
@@ -144,7 +144,14 @@

   if bufferPath and build.node
     {id} = processed[bufferPath]
-    result += "\nvar Buffer = require('#{id}').Buffer;"
+    result += "\nvar Buffer = require('#{id}').Buffer;\n"
+    result +=
+      """
+      if (!Buffer.prototype.toJSON)
+        Buffer.prototype.toJSON = function() {
+          return Array.prototype.slice.call(this, 0);
+        };
+      """

   if consolePath and build.node
     {id} = processed[consolePath]


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera.coffee
+++ src/camera.coffee
@@ -36,7 +36,7 @@
       height: 2 * deadzone.y

     if objectCenter.x > centerRect.right
-
+      I.scroll.
     else if objectCenter.x < centerRect.left


error handling

error handling

--- src/trello.coffee
+++ src/trello.coffee
@@ -49,13 +49,17 @@
   ensureConfig msg.send
   trello.get "/1/organizations/#{process.env.HUBOT_TRELLO_ORGANIZATION}/boards", (err, data) ->
     msg.reply "There was an error reading the list of boards" if err
+    found_board = false
     for board in data
       if board.name.toLowerCase() == msg.envelope.room.toLowerCase()
+        found_board = true
         msg.reply "I've found the board #{board.name}. Now looking for the list..."
         trello.get "/1/boards/#{board.id}/lists", (err, data) ->
           msg.reply "There was an error reading the lists" if err
           msg.send "* #{list.name}" for list in data unless err and data.length == 0
         break
+    if found_board == false
+      msg.reply "I couldn't find a board named: #{msg.envelope.room}." unless id

 createCard = (msg, list_name, cardName) ->
   msg.reply "Sure thing boss. I'll create that card for you."


Process getOrderBook result

Process getOrderBook result

--- src/client.coffee
+++ src/client.coffee
@@ -37,7 +37,13 @@
     req = @auth.authorize(@builder.buildRequest(method, path, opts), opts)
     @handler.do(req, opts)

-  getOrderBook: (opts) => @pipeline('GET', 'orderbook', auth: false, opts)
+  getOrderBook: (opts) =>
+    @pipeline('GET', 'orderbook', auth: false, opts).then (res) ->
+      if res.result.statusCode is 200
+        JSON.parse(res.result.body)
+      else
+        res
+
   getTradeHistory: (opts) => @pipeline('GET', 'trades', auth: false, opts)
   getTicker: (opts) => @pipeline('GET', 'ticker', auth: false, opts)


Ember runloop snippets

Ember runloop snippets


Add local variable for clarity/readability

--- src/trix/models/document.coffee
+++ src/trix/models/document.coffee
@@ -218,11 +218,12 @@

   applyBlockAttributeAtRange: (attributeName, value, range) ->
     {document, range} = @expandRangeToLineBreaksAndSplitBlocks(range)
-
-    if getBlockAttributes()[attributeName].listAttribute
+    attribute = getBlockAttributes()[attributeName]
+
+    if attribute.listAttribute
       document = document.removeLastListAttributeAtRange(range, exceptAttributeName: attributeName)
       {document, range} = document.convertLineBreaksToBlockBreaksInRange(range)
-    else if getBlockAttributes()[attributeName].terminal
+    else if attribute.terminal
       {document, range} = document.convertLineBreaksToBlockBreaksInRange(range)
     else
       document = document.consolidateBlocksAtRange(range)


fixed another silly copy-paste bug

fixed another silly copy-paste bug

--- src/common.coffee
+++ src/common.coffee
@@ -166,7 +166,7 @@

   rings2geoJson: (rings0)->
     rings = rings0
-      .map (ring,i)-> if i is 0 then C.makeCcw(ring) else C.makeCw(ring)
+      .map (ring,i)-> if i is 0 then C.makeRingCcw(ring) else C.makeRingCw(ring)
       .map C.closeRing

     type: "Feature"


lint jsx files, too

lint jsx files, too

--- app/templates/gulp/tasks/scripts.coffee
+++ app/templates/gulp/tasks/scripts.coffee
@@ -56,6 +56,7 @@
 gulp.task 'scripts:lint',  () ->
   files = [
     "#{path.dev.js}/**/!(*.bundle).js"
+    "#{path.dev.js}/**/*.jsx"
   ]

   gulp.src(files)


Cosmetic [ci skip]

Cosmetic [ci skip]

--- app/views/identifiers/convert.js.coffee
+++ app/views/identifiers/convert.js.coffee
@@ -6,4 +6,4 @@
 """
 <% end %>

-$("table#mapped-ids tbody").html("#{j(render 'results')}")
+$("table#mapped-ids tbody").html "#{j(render 'results')}"


menus: MyShows: colors!

menus: MyShows: colors!


:art: s/N1/Nylas Mail/ in window-event-handler.coffee

--- src/window-event-handler.coffee
+++ src/window-event-handler.coffee
@@ -224,7 +224,7 @@
     return unless NylasEnv.isMainWindow()

     if !NylasEnv.inDevMode()
-      console.log("%c Welcome to N1! If you're exploring the source or building a
+      console.log("%c Welcome to Nylas Mail! If you're exploring the source or building a
                    plugin, you should enable debug flags. It's slower, but
                    gives you better exceptions, the debug version of React,
                    and more. Choose %c Developer > Run with Debug Flags %c


fix key suggestions

fix key suggestions

--- app/editor.coffee
+++ app/editor.coffee
@@ -160,7 +160,7 @@
         unless next_token?.string is ':'
           key_suggestions = _.map key_suggestions, (k) -> k + ':'

-        list.concat key_suggestions
+        list.push key_suggestions...
       list

     list = collect_suggestions full_s


:art: Reorder args to be consistent with other methods

--- src/config.coffee
+++ src/config.coffee
@@ -602,7 +602,7 @@
         return false

     if scopeSelector?
-      @setRawScopedValue(source, scopeSelector, keyPath, value)
+      @setRawScopedValue(keyPath, value, source, scopeSelector)
     else
       @setRawValue(keyPath, value)

@@ -1057,7 +1057,7 @@
       disposable.dispose()
       @emitChangeEvent()

-  setRawScopedValue: (source, selector, keyPath, value) ->
+  setRawScopedValue: (keyPath, value, source, selector, options) ->
     if keyPath?
       newValue = {}
       _.setValueForKeyPath(newValue, keyPath, value)


Doc :lipstick:

Doc :lipstick:

--- src/config.coffee
+++ src/config.coffee
@@ -12,10 +12,10 @@
 #
 # An instance of this class is always available as the `atom.config` global.
 #
-# ## Getting and setting config settings
-#
-# ```coffee
-# # With no value set, `::get` returns the setting's default value
+# ## Getting and setting config settings. Note that with no value set, {::get}
+# returns the setting's default value.
+#
+# ```coffee
 # atom.config.get('my-package.myKey') # -> 'defaultValue'
 #
 # atom.config.set('my-package.myKey', 'value')
@@ -244,7 +244,7 @@
 #
 # #### title and description
 #
-# The settings view will use the `title` and `description` keys display your
+# The settings view will use the `title` and `description` keys to display your
 # config setting in a readable way. By default the settings view humanizes your
 # config key, so `someSetting` becomes `Some Setting`. In some cases, this is
 # confusing for users, and a more descriptive title is useful.
@@ -260,7 +260,7 @@
 #     default: 4
 # ```
 #
-# __Note__: You should strive to be so clear in your naming of the  setting that
+# __Note__: You should strive to be so clear in your naming of the setting that
 # you do not need to specify a title or description!
 #
 # ## Best practices


Pass previous value to the callback in config.observe()

--- src/config.coffee
+++ src/config.coffee
@@ -207,8 +207,9 @@
     updateCallback = =>
       value = @get(keyPath)
       unless _.isEqual(value, previousValue)
+        previous = previousValue
         previousValue = _.clone(value)
-        callback(value)
+        callback(value, {previous})

     subscription = { cancel: => @off 'updated', updateCallback  }
     @on 'updated', updateCallback


Join dirs in config LESS path

Join dirs in config LESS path

--- src/config.coffee
+++ src/config.coffee
@@ -46,7 +46,7 @@
   packageDirPaths: _.clone(userPackageDirPaths)
   userPackageDirPaths: userPackageDirPaths
   userStoragePath: userStoragePath
-  lessSearchPaths: [path.join(resourcePath, 'static/less-imports'), path.join(resourcePath, 'static'), path.join(resourcePath, 'vendor')]
+  lessSearchPaths: [path.join(resourcePath, 'static', 'less-imports'), path.join(resourcePath, 'static'), path.join(resourcePath, 'vendor')]
   defaultSettings: null
   settings: null
   configFileHasErrors: null


Stub game

Stub game

--- src/config.coffee
+++ src/config.coffee
@@ -1,4 +1,5 @@
 {layout} = require('./config/layout')
+{game} = require('./config/game')
 {grid} = require('./config/grid')
 {turtles} = require('./config/turtles')
 {inspector} = require('./config/inspector')
@@ -34,6 +35,7 @@
     reset: {name: "|<", do: "reset"}
   }
   _CHILDREN: [
+    game
     grid
     turtles
     inspector


Ensure set/unset operations take place after user's config is loaded

--- src/config.coffee
+++ src/config.coffee
@@ -399,6 +399,8 @@

   # Created during initialization, available as `atom.config`
   constructor: ({@notificationManager, @enablePersistence}={}) ->
+    @settingsLoaded = false
+    @pendingOperations = []
     @clear()

   initialize: ({@configDirPath, @resourcePath, projectHomeSchema}) ->
@@ -646,6 +648,10 @@
   # * `true` if the value was set.
   # * `false` if the value was not able to be coerced to the type specified in the setting's schema.
   set: ->
+    unless @settingsLoaded
+      @pendingOperations.push(() => @set.apply(@, arguments))
+      return
+
     [keyPath, value, options] = arguments
     scopeSelector = options?.scopeSelector
     source = options?.source
@@ -677,6 +683,10 @@
   #   * `scopeSelector` (optional) {String}. See {::set}
   #   * `source` (optional) {String}. See {::set}
   unset: (keyPath, options) ->
+    unless @settingsLoaded
+      @pendingOperations.push(() => @unset.apply(@, arguments))
+      return
+
     {scopeSelector, source} = options ? {}
     source ?= @getUserConfigPath()

@@ -944,7 +954,12 @@

     @transact =>
       @settings = {}
+      @settingsLoaded = true
       @set(key, value, save: false) for key, value of newSettings
+      if @pendingOperations.length
+        op() for op in @pendingOperations
+        @debouncedSave()
+        @pendingOperations = []
       return

   getRawValue: (keyPath, options) ->


Failing test for string interpolation. Interpolated strings need to be expressions, not values.

--- test/test_string_interpolation.coffee
+++ test/test_string_interpolation.coffee
@@ -48,8 +48,10 @@
 obj: {
   name: 'Joe'
   hi: -> "Hello $@name."
+  cya: -> "Hello $@name.".replace('Hello','Goodbye')
 }
 ok obj.hi() is "Hello Joe."
+ok obj.cya() is "Goodbye Joe."

 ok "With ${"quotes"}" is 'With quotes'
 ok 'With ${"quotes"}' is 'With ${"quotes"}'


We are not using those events anymore.

We are not using those events anymore.

--- client/lib/highlighter.coffee
+++ client/lib/highlighter.coffee
@@ -334,17 +334,7 @@
     $(window).on 'scroll.highlighter', @checkRender
     $(window).on 'resize.highlighter', @checkRender

-    $(document).on 'mousedown.highlighter', (e) =>
-      @mouseDown = true if e.which is 1 # Left mouse button
-      return # Make sure CoffeeScript does not return anything
-    $(document).on 'mouseup.highlighter', (e) =>
-      @mouseDown = false
-      return # Make sure CoffeeScript does not return anything
-
   destroy: =>
-    $(document).off 'mousedown.highlighter'
-    $(document).off 'mouseup.highlighter'
-
     $(window).off 'scroll.highlighter'
     $(window).off 'resize.highlighter'


update w3d product

update w3d product

--- assets/coffeescript/product_list.coffee
+++ assets/coffeescript/product_list.coffee
@@ -292,21 +292,21 @@
       img: [pupil_img_path,'pupil_w120_e120b.jpg'].join('/')

     pupil_w3d_enone:
-      cost: 1440
+      cost: 1540
       title_product: 'Pupil Headset'
       sub_products:
         world_camera: world_cam_data.w3d
         eye_camera: eye_cam_data.enone
       img: [pupil_img_path,'pupil_w3d_enone.jpg'].join('/')
     pupil_w3d_e120:
-      cost: 1890
+      cost: 1990
       title_product: 'Pupil Headset'
       sub_products:
         world_camera: world_cam_data.w3d
         eye_camera: eye_cam_data.e120
       img: [pupil_img_path,'pupil_w3d_e120.jpg'].join('/')
     pupil_w3d_e120b:
-      cost: 2590
+      cost: 2690
       title_product: 'Pupil Headset'
       sub_products:
         world_camera: world_cam_data.w3d
@@ -314,7 +314,7 @@
       img: [pupil_img_path,'pupil_w3d_e120b.jpg'].join('/')

     pupil_w3d_enone_edu:
-      cost: 850
+      cost: 950
       title_product: 'Pupil Headset'
       sub_products:
         world_camera: world_cam_data.w3d
@@ -322,7 +322,7 @@
         discount: edu_discount
       img: [pupil_img_path,'pupil_w3d_enone.jpg'].join('/')
     pupil_w3d_e120_edu:
-      cost: 1300
+      cost: 1400
       title_product: 'Pupil Headset'
       sub_products:
         world_camera: world_cam_data.w3d
@@ -330,7 +330,7 @@
         discount: edu_discount
       img: [pupil_img_path,'pupil_w3d_e120.jpg'].join('/')
     pupil_w3d_e120b_edu:
-      cost: 2000
+      cost: 2100
       title_product: 'Pupil Headset'
       sub_products:
         world_camera: world_cam_data.w3d


you can open directories

you can open directories

(we should open directories in new windows, but baby steps)

--- src/editor.coffee
+++ src/editor.coffee
@@ -33,13 +33,20 @@
   setMode()
   Chrome.setDirty false
 open = ->
-  if /png|jpe?g|gif/i.test filename
-    Chrome.openURL filename
-  else
+  if Dir.isDir(filename)
+    Process.cwd(filename)
     Chrome.title _.last filename.split('/')
-    editor.getSession().setValue File.read filename
+    editor.getSession().setValue ""
     setMode()
     Chrome.setDirty false
+  else
+    if /png|jpe?g|gif/i.test filename
+      Chrome.openURL filename
+    else
+      Chrome.title _.last filename.split('/')
+      editor.getSession().setValue File.read filename
+      setMode()
+      Chrome.setDirty false
 setMode = ->
   if /\.js$/.test filename
     editor.getSession().setMode new JavaScriptMode
@@ -78,7 +85,6 @@
   if filename then save() else saveAs()

 bindKey 'new', 'Command-N', (env, args, request) ->
-  console.log 'hi mom'
   Chrome.createWindow()

 bindKey 'copy', 'Command-C', (env, args, request) ->


TextNodeFielter -> TextNodeFilter

TextNodeFielter -> TextNodeFilter

--- src/editor.coffee
+++ src/editor.coffee
@@ -10,7 +10,7 @@
 _ = require './underscore-extensions'

 MeasureRange = document.createRange()
-TextNodeFileter = { acceptNode: -> NodeFilter.FILTER_ACCEPT }
+TextNodeFilter = { acceptNode: -> NodeFilter.FILTER_ACCEPT }
 NoScope = ['no-scope']

 # Private: Represents the entire visual pane in Atom.
@@ -1576,7 +1576,7 @@

   measureToColumn: (lineElement, tokenizedLine, column) ->
     left = oldLeft = index = 0
-    iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, TextNodeFileter)
+    iterator = document.createNodeIterator(lineElement, NodeFilter.SHOW_TEXT, TextNodeFilter)

     returnLeft = null


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/engine.coffee
+++ src/engine.coffee
@@ -105,7 +105,6 @@
     Object.reverseMerge I,
       FPS: 60
       paused: false
-      showFPS: false

     frameAdvance = false


Input: Implemented the correct usage of 'autocomplete' attribute. See #63251

--- src/elements/Input/Input.coffee
+++ src/elements/Input/Input.coffee
@@ -70,8 +70,8 @@
 				default: false
 				check: Boolean
 			autocomplete:
-				default: false
-				check: Boolean
+				default: "off"
+				check: String
 			overwrite:
 				check: Boolean
 			checkInput:
@@ -155,7 +155,7 @@
 			CUI.util.assert(not (@opts.getCursorBlocks or @opts.getInputBlocks or @opts.checkInput), "new CUI.Input", "opts.readonly conflicts with opts.getCursorBlocks, opts.getInputBlocks, opts.checkInput.")

 		if @opts.textarea
-			CUI.util.assert(not @opts.autocomplete, "new CUI.Input", "opts.textarea does not work with opts.autocomplete", opts: @opts)
+			CUI.util.assert(@opts.autocomplete != "off", "new CUI.Input", "opts.textarea does not work with opts.autocomplete", opts: @opts)
 			CUI.util.assert(not @opts.incNumbers, "new CUI.Input", "opts.textarea does not work with opts.incNumbers", opts: @opts)

 		super()
@@ -192,11 +192,8 @@
 		# if @_rows
 		# 	CUI.util.assert(@_content_size, "new CUI.Input", "opts.rows can only be used with opts.content_size set.", opts: @opts)

-		if @_autocomplete == true
-			@__autocomplete = "on"
-		else if @_autocomplete == false
-			@__autocomplete = "off"
-
+		# Available autocomplete values here: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete
+		@__autocomplete = @_autocomplete
 		@

 	__checkInputRegexp: (value) ->


Panel: Add more parameters to the onFirstActivate callback.

The callback now received the same paramters as onActivate: btn, flags, event.

--- src/elements/Panel/Panel.coffee
+++ src/elements/Panel/Panel.coffee
@@ -45,7 +45,7 @@
 			onActivate: (btn, flags, event) =>
 				@__activations++
 				if @__activations == 1
-					@_onFirstActivate?(@)
+					@_onFirstActivate?(@, flags, event)

 				@__open(not flags.initial_activate)
 				@_onActivate?(btn, flags, event)


Cache compiled scripts

Cache compiled scripts

--- layout.coffee
+++ layout.coffee
@@ -184,10 +184,14 @@
     return script unless compiler of compilermappings
     script = compilermappings[compiler](script)

+  scriptCache = {}
   compileScript = (script='', args=[]) ->
+    key = script + args.join()
+    return scriptCache[key] if key of scriptCache
     # console.log 'compileScript', compiler, args, script
     try
-      new Function(args, script)
+      # console.log scriptCache
+      scriptCache[key] = new Function(args, script)
     catch e
       console.error('failed to compile', args, script, e)


U-Wing and TIE Striker

U-Wing and TIE Striker

-Added the translation of both expansion names.
-Added the translation of both ship names.
-Translated all the cards of both expansions, adding :ship: tags when needed to avoid problems.

--- coffeescripts/cards-es.coffee
+++ coffeescripts/cards-es.coffee
@@ -79,6 +79,8 @@
         "Shadow Caster Expansion Pack": "Pack de Expansión Sombra Alargada"
         "Special Forces TIE Expansion Pack": "Pack de Expansión TIE de las Fuerzas Especiales"
         "ARC-170 Expansion Pack": "Pack de Expansión ARC-170"
+        "U-Wing Expansion Pack": "Pack de Expansión Ala-U"
+        "TIE Striker Expansion Pack": "Pack de Expansión Fustigador TIE"
     ui:
         shipSelectorPlaceholder: "Selecciona una nave"
         pilotSelectorPlaceholder: "Selecciona un piloto"
@@ -190,6 +192,7 @@
     exportObj.renameShip 'VT-49 Decimator', 'VT-49 Diezmador'
     exportObj.renameShip 'TIE/fo Fighter', 'Caza TIE/fo'
     exportObj.renameShip 'TIE/sf Fighter', 'Caza TIE/sf'
+    exportObj.renameShip 'TIE Striker', 'Fustigador TIE'
     exportObj.renameShip 'A-Wing', 'Ala-A'
     exportObj.renameShip 'B-Wing', 'Ala-B'
     exportObj.renameShip 'E-Wing', 'Ala-E'
@@ -202,6 +205,7 @@
     exportObj.renameShip 'CR90 Corvette (Fore)', 'Corbeta CR90 (Proa)'
     exportObj.renameShip 'GR-75 Medium Transport', 'Transporte mediano GR-75'
     exportObj.renameShip 'T-70 X-Wing', 'T-70 Ala-X'
+    exportObj.renameShip 'U-Wing', 'Ala-U'
     exportObj.renameShip 'M3-A Interceptor', 'Interceptor M3-A'
     exportObj.renameShip 'StarViper', 'Víbora Estelar'
     exportObj.renameShip 'Aggressor', 'Agresor'
@@ -889,17 +893,26 @@
         'Unkar Plutt':
             text: '''At the end of the Activation phase, you must assign a tractor beam token to each ship you are touching.'''
         'Cassian Andor':
-            text: '''At the start of the Activation phase, you may remove 1 stress token from 1 other friendly ship at Range 1-2.'''
+            ship: "Ala-U"
+            text: '''Al comienzo de la fase de Activación, puedes quitar 1 ficha de Tensión de 1 otra nave aliada que tengas a alcance 1-2.'''
         'Bodhi Rook':
-            text: '''When a friendly ship acquires a target lock, that ship can lock onto an enemy ship at Range 1-3 of any friendly ship.'''
+            ship: "Ala-U"
+            text: '''Cuando una nave aliada fije un blanco, esa nave puede fijarlo sobre una nave enemiga que esté situada a alcance 1-3 de cualquier nave aliada.'''
         'Heff Tobber':
-            text: '''After an enemy ship executes a maneuver that causes it to overlap your ship, you may perform a free action.'''
+            ship: "Ala-U"
+            text: '''Después de que una nave enemiga ejecute una maniobra que la haga solaparse con tu nave, puedes realizar una acción gratuita.'''
         '''"Duchess"''':
-            text: '''While you have the "Adaptive Ailerons" Upgrade card equipped, you may choose to ignore its card ability.'''
+            ship: "Fustigador TIE"
+            name: '"Duquesa"'
+            text: '''Cuando tengas equipada la carta de Mejora "Alreones adaptativos", puedes elegir ignorar su capacidad de carta.'''
         '''"Pure Sabacc"''':
-            text: '''When attacking, if you have 1 or fewer Damage cards, roll 1 additional attack die.'''
+            ship: "Fustigador TIE"
+            name: '"Sabacc Puro"'
+            text: '''Cuando ataques, si tienes 1 o menos cartas de Daño, tira 1 dado de ataque adicional.'''
         '''"Countdown"''':
-            text: '''When defending, if you are not stressed, during the "Compare Results" step, you may suffer 1 damage to cancel all dice results.  If you do, receive 1 stress token.'''
+            ship: "Fustigador TIE"
+            name: '"Cuenta Atrás"'
+            text: '''Cuando te defiendas, si no estás bajo tensión, durante el paso "Comparar los resultados", puedes sufrir 1 punto de daño para anular todos los resultados de los dados. Si lo haces, recibes 1 ficha de Tensión.'''
         'Nien Nunb':
             ship: "T-70 Ala-X"
             text: '''Cuando recibas una ficha de Tensión, si hay alguna nave enemiga dentro de tu arco de fuego a alcance 1, puedes descartar esa ficha de Tensión.'''
@@ -923,6 +936,18 @@
             text: '''When you reveal a reverse maneuver, you may drop a bomb using your front guides (including a bomb with the "Action:" header).'''
         'Sarco Plank':
             text: '''When defending, instead of using your agility value, you may roll a number of defense dice equal to the speed of the maneuver you executed this round.'''
+        "Blue Squadron Pathfinder":
+            name: "Infiltrador del Escuadrón Azul"
+            ship: "Ala-U"
+        "Black Squadron Scout":
+            name: "Explorador del Escuadrón Negro"
+            ship: "Fustigador TIE"
+        "Scarif Defender":
+            name: "Defensor de Scarif"
+            ship: "Fustigador TIE"
+        "Imperial Trainee":
+            name: "Cadete Imperial"
+            ship: "Fustigador TIE"

     upgrade_translations =
         "Ion Cannon Turret":
@@ -1475,9 +1500,9 @@
         'A Score to Settle':
             text: '''During setup, before the "Place Forces" step, choose 1 enemy ship and deal the "A Debt to Pay" Condition card to it.%LINEBREAK%Wehn attacking a ship that has the "A Debt to Pay" Condition card, you may change 1 %FOCUS% result to a %CRIT% result.'''
         'Jyn Erso':
-            text: '''%REBELONLY%%LINEBREAK%Action: Choose 1 friendly ship at Range 1-2. Assign 1 focus token to that ship for each enemy ship inside your firing arc at Range 1-3.  You cannot assign more than 3 focus tokens in this way.'''
+            text: '''%REBELONLY%%LINEBREAK%Acción: Elige 1 nave aliada que tengas a alcance 1-2. Asigna 1 ficha de Concentración a esa nave por cada nave enemiga que tengas dentro de tu arco de fuego a alcance 1-3. No puedes asignar más de 3 fichas de esta forma.'''
         'Cassian Andor':
-            text: '''%REBELONLY%%LINEBREAK%At the end of the Planning phase, you may choose an enemy ship at Range 1-2.  Guess aloud that ship's bearing and speed, then look at its dial.  If you are correct, you may rotate your dial to another maneuver.'''
+            text: '''%REBELONLY%%LINEBREAK%Al final de la fase de Planificación, puedes elegir una nave enemiga que tengas a alcance 1-2. Di en voz alta la dirección y velocidad que crees que va a tener esa nave, y luego mira su selector d emaniobras. Si aciertas, puedes girar la rueda de tu selector para asignarle otra maniobra.'''
         'Finn':
             text: '''%REBELONLY%%LINEBREAK%Cuando ataques con un armamento principal o te defiendas, si la nave enemiga está dentro de tu arco de fuego, puedes añadir 1 resultado de cara vacía a tu tirada.'''
         'Rey':
@@ -1517,17 +1542,20 @@
         '''Scavenger Crane''':
             text: '''After a ship at Range 1-2 is destroyed, you may choose a discarded %TORPEDO%, %MISSILE%, %BOMB%, %CANNON%, %TURRET%, or Modification Upgrade card that was equipped to your ship and flip it faceup.  Then roll 1 attack die.  On a blank result, discard Scavenger Crane.'''
         'Bodhi Rook':
-            text: '''%REBELONLY%%LINEBREAK%When you acquire a target lock, you can lock onto an enemy ship at Range 1-3 of any friendly ship.'''
+            text: '''%REBELONLY%%LINEBREAK%Cuando fijes un blanco, puedes fijarlo sobre una nave enemiga que esté situada a alcance 1-3 de cualquier nave aliada.'''
         'Baze Malbus':
-            text: '''%REBELONLY%%LINEBREAK%After you perform an attack that does not hit, you may immediately perform a primary weapon attack against a different ship.  You cannot perform another attack this round.'''
+            text: '''%REBELONLY%%LINEBREAK%Después de que efectúes un ataque que no impacte, puedes realizar inmediatamente un ataque con tu armamento principal contra una nave diferente. No podrás realizar ningún otro ataque en esta misma ronda.'''
         'Inspiring Recruit':
-            text: '''Once per round, when a friendly ship at Range 1-2 removes a stress token, it may remove 1 additional stress token.'''
+            name: "Recluta inspirador"
+            text: '''Una vez por ronda, cuando una nave aliada que tengas a alcance 1-2 se quite una ficha de Tensión, puede quitarse 1 ficha de Tensión adicional.'''
         'Swarm Leader':
-            text: '''When performing a primary weapon attack, choose up to 2 other friendly ships that have the defender inside their firing arcs at Range 1-3. Remove 1 evade token from each chosen ship to roll 1 additional attack die for each token removed.'''
+            name: "Jefe de enjambre"
+            text: '''Cuando ataques con un armamento principal, elige hasta 2 ortas naves aliadas que tengan al defensor dentro de sus arcos de fuego a alcance 1-3. Quita 1 ficha de Evasión de cada nave elegida para tirar 1 dado de ataque adicional por cada ficha quitada.'''
         'Bistan':
-            text: '''%REBELONLY%%LINEBREAK%When attacking Range 1-2, you may change 1 of your %HIT% results to a %CRIT% result.'''
+            text: '''%REBELONLY%%LINEBREAK%Cuando ataques a alcance 1-2, puedes cambiar 1 de tus resultados %HIT% por un resultado %CRIT%.'''
         'Expertise':
-            text: '''When attacking, if you are not stressed, you may change all of your %FOCUS% results to %HIT% results.'''
+            name: "Maestría"
+            text: '''Cuando ataques, si no estás bajo tensión, puedes cambair todos tus resultados %FOCUS% por resultados %HIT%.'''
         'BoShek':
             text: '''When a ship you are touching activates, you may look at its chosen maneuver.  If you do, its owner must rotate the dial to an adjacent maneuver.  The ship can reveal and execute that maneuver even while stressed.'''
         # C-ROC
@@ -1629,7 +1657,8 @@
         'Spacetug Tractor Array':
             text: '''Quadjumper only.%LINEBREAK%Action: Choose a ship inside your firing arc at Range 1 and assign a tractor beam token to it.  If it is a friendly ship, resolve the effect of the tractor beam token as though it were an enemy ship.'''
         'Lightweight Frame':
-            text: '''TIE only.%LINEBREAK%When defending, after rolling defense dice, if there are more attack dice than defense dice, roll 1 additional defense die.%LINEBREAK%You cannot equip this card if your agility value is "3" or higher.'''
+            name: "Fuselaje ultraligero"
+            text: '''Sólo TIE.%LINEBREAK%Cuando te defiendas, tras tirar los dados de defensa, si hay más dados de ataque que dados de defensa, tira 1 dado de defensa adicional.%LINEBREAK%Esta mejora no puede equiparse en naves con puntuación de Agilidad 3 o superior.'''

     title_translations =
         "Slave I":
@@ -1786,9 +1815,11 @@
         '''Kylo Ren's Shuttle''':
             text: '''Upsilon-class Shuttle only.%LINEBREAK%At the end of the Combat phase, choose an unstressed enemy ship at Range 1-2.  Its owner must assign a stress token to it or assign a stress token to another ship at Range 1-2 of you that that player controls.'''
         '''Pivot Wing''':
-            text: '''U-Wing only. %DUALCARD%%LINEBREAK%Side A (Attack): Increase your agility by 1.%LINEBREAK%After you execute a maneuver, you may flip this card.%LINEBREAK%Side B (Landing): When you reveal a (0 %STOP%) maneuver, you may rotate your ship 180°.%LINEBREAK%After you execute a maneuver, you may flip this card.'''
+            ship: "Ala-U"
+            text: '''Sólo Ala-U. %DUALCARD%%LINEBREAK%Cara A (Ataque): Tu Agilidad se incrementa en 1.%LINEBREAK%Después de que ejecutes una maniobra, puedes darle la vuelta a esta carta.%LINEBREAK%Cara B (Aterrizaje): Cuando reveles una maniobra (%STOP% 0), puedes cambiar la orientación de tu nave en 180°.%LINEBREAK%Después de que ejecutes una maniobra, puedes darle la vuelta a esta carta.'''
         '''Adaptive Ailerons''':
-            text: '''TIE Striker only.%LINEBREAK%Immediately before you reveal your dial, if you are not stressed, you must execute a white (%BANKLEFT% 1), (%STRAIGHT% 1), or (%BANKRIGHT% 1) maneuver.'''
+            name: "Alerones adaptativos"
+            text: '''Sólo Fustigador TIE.%LINEBREAK%Inmediatamente antes de revelar tu selector de maniobras, si no estás bajo tensión, debes ejecutar una manibora blanca (%BANKLEFT% 1), (%STRAIGHT% 1) o (%BANKRIGHT% 1).'''
         # C-ROC
         '''Merchant One''':
             text: '''C-ROC Cruiser only.%LINEBREAK%Your upgrade bar 1 additional %CREW% upgrade icon and 1 additional %TEAM% upgrade icon and loses 1 %CARGO% upgrade icon.'''