various changes

various changes

--- src/math3d.coffee
+++ src/math3d.coffee
@@ -73,10 +73,11 @@
 # WARNING -- don't accidentally use this as a default:
 required = defaults.required = "__!!!!!!this is a required property!!!!!!__"

-component_to_hex = (component) ->
-    (if component < 16 then "0" else "") + component.toString 16
-
-rgb_to_hex = (r, g, b) -> "#" + component_to_hex(r) + component_to_hex(g) + component_to_hex(b)
+rescale_to_hex = (val) ->
+    val = Math.round 255*val
+    (if val < 16 then "0" else "") + val.toString 16
+
+rgb_to_hex = (rgb) -> '#'+rgb.map(rescale_to_hex).join('')

 remove_element = (element) ->
     if element? and (parent = element.parentElement)?
@@ -179,17 +180,19 @@
             parent          : required
             width           : undefined
             height          : undefined
-            renderer        : undefined  # 'webgl' or 'canvas' or undefined to choose best
+            renderer        : undefined # 'webgl' or 'canvas' or undefined to choose best
             background      : [1,1,1]
-            foreground      : undefined  # defaults to the HSL negation of the background
-            frame_color     : undefined  # defaults to the foreground color
-            frame_thickness : .4         # zero thickness disables the frame
-            label_fontsize  : 14         # set to zero to disable labels on the axes
-            spin            : false      # if true, image spins by itself when mouse is over it.
+            spin            : false     # if true, image spins by itself when mouse is over it.
             camera_distance : 10
-            aspect_ratio    : undefined  # undefined does nothing or a triple [x,y,z] of length three, which scales the x,y,z coordinates of everything by the given values.
-            stop_when_gone  : undefined  # if given, animation, etc., stops when this html element (not jquery!) is no longer in the DOM
-            callback        : undefined  # opts.callback(error, this object)
+            aspect_ratio    : undefined # undefined does nothing or a triple [x,y,z] of length three, which scales the x,y,z coordinates of everything by the given values.
+            stop_when_gone  : undefined # if given, animation, etc., stops when this html element (not jquery!) is no longer in the DOM
+            frame           : undefined # frame options
+            callback        : undefined # opts.callback(error, this object)
+
+        @frameOpts = defaults @opts.frame,
+            color           : undefined # defaults to the color-wise negation of the background
+            thickness       : .4        # zero thickness disables the frame
+            fontsize        : 14        # zero fontsize disables labels on the axes

         math3d.load_threejs (error) =>
             if error
@@ -222,23 +225,14 @@
             @init_light()

             # set background color
-            @opts.background = new THREE.Color @opts.background...
-            @element.style.background = @opts.background.getStyle()
-
-            if @opts.foreground?
-                @opts.foreground = new THREE.Color @opts.foreground...
+            @background = new THREE.Color @opts.background...
+            @element.style.background = @background.getStyle()
+
+            if @frameOpts.color?
+                @frameColor = new THREE.Color @frameOpts.color...
             else
-                @opts.foreground = new THREE.Color()
-                {h, s, l} = @opts.background.getHSL()
-                if h >= 0.5
-                    @opts.foreground.setHSL(h-0.5, s, 1-l)
-                else
-                    @opts.foreground.setHSL(h+0.5, s, 1-l)
-
-            if @opts.frame_color?
-                @opts.frame_color = new THREE.Color @opts.frame_color...
-            else
-                @opts.frame_color = @opts.foreground
+                @frameColor = new THREE.Color(
+                    1-@background.r, 1-@background.g, 1-@background.b)

             @opts.callback? undefined, @

@@ -254,13 +248,7 @@
     # client code should call this when done adding objects to the scene
     finalize: ->
         @set_frame()
-
-        if @renderer_type isnt 'dynamic'
-            # if we don't have the renderer, swap it in, make a static image, then give it back to whoever had it.
-            owner = _scene_using_renderer
-            @set_dynamic_renderer()
-            @set_static_renderer()
-            owner?.set_dynamic_renderer()
+        @render_scene()

         # possibly show the canvas warning.
         if @opts.renderer is 'canvas'
@@ -281,7 +269,7 @@
         # place renderer in correct place in the DOM
         @element.appendChild @renderer.domElement

-        @renderer.setClearColor @opts.background, 1
+        @renderer.setClearColor @background, 1
         @renderer.setSize @opts.width, @opts.height
         if @controls?
             @controls.enabled = true
@@ -404,15 +392,15 @@
         @light = new THREE.PointLight color
         @light.position.set 0, d, 0

-    update_bounding_box: (obj) ->
+    updateBoundingBox: (obj) ->
         if not obj.geometry.boundingBox?
             obj.geometry.computeBoundingBox()
-        if @bounding_box?
-            @bounding_box.geometry.boundingBox.union obj.geometry.boundingBox
+        if @boundingBox?
+            @boundingBox.geometry.boundingBox.union obj.geometry.boundingBox
         else
-            @bounding_box = new THREE.BoxHelper()
-            @bounding_box.geometry.boundingBox = obj.geometry.boundingBox.clone()
-        @bounding_box.update @bounding_box
+            @boundingBox = new THREE.BoxHelper()
+            @boundingBox.geometry.boundingBox = obj.geometry.boundingBox.clone()
+        @boundingBox.update @boundingBox

     add_text: (opts) ->
         opts = defaults opts,
@@ -420,7 +408,7 @@
             text             : required
             fontsize         : 12
             fontface         : 'Arial'
-            color            : "#000000"   # anything that is valid to canvas context, e.g., "rgba(249,95,95,0.7)" is also valid.
+            material         : required
             constant_size    : true  # if true, then text is automatically resized when the camera moves;
             in_frame         : true
             # WARNING: if constant_size, don't remove text from scene (or if you do, note that it is slightly inefficient still.)
@@ -431,14 +419,14 @@
         canvas = document.createElement 'canvas'
         canvas.width = width
         canvas.height = height
-        context = canvas.getContext "2d"  # get the drawing context
+        context = canvas.getContext '2d'  # get the drawing context

         # set the fontsize and fix for our text.
-        context.font = "Normal " + opts.fontsize + "px " + opts.fontface
+        context.font = 'Normal ' + opts.fontsize + 'px ' + opts.fontface
         context.textAlign = 'center'

         # set the color of our text
-        context.fillStyle = opts.color
+        context.fillStyle = rgb_to_hex opts.material.color

         # actually draw the text -- right in the middle of the canvas.
         context.fillText opts.text, width/2, height/2
@@ -448,7 +436,9 @@
         texture.needsUpdate = true

         # Make a material out of our texture.
-        spriteMaterial = new THREE.SpriteMaterial map: texture
+        spriteMaterial = new THREE.SpriteMaterial
+            map     : texture
+            opacity : opts.material.opacity

         # Make the sprite itself.  (A sprite is a 3d plane that always faces the camera.)
         sprite = new THREE.Sprite spriteMaterial
@@ -467,7 +457,7 @@

         # Finally add the sprite to our scene
         if opts.in_frame
-            @update_bounding_box sprite
+            @updateBoundingBox sprite
         @scene.add sprite

     add_line: (opts) ->
@@ -486,7 +476,7 @@
         line.material.color.setRGB opts.material.color

         if opts.in_frame
-            @update_bounding_box line
+            @updateBoundingBox line
         @scene.add line

     add_point: (opts) ->
@@ -519,12 +509,14 @@

                 context.beginPath()
                 context.arc centerX, centerY, radius, 0, 2*Math.PI, false
-                context.fillStyle = rgb_to_hex opts.material.color...
+                context.fillStyle = rgb_to_hex opts.material.color
                 context.fill()

                 texture = new THREE.Texture canvas
                 texture.needsUpdate = true
-                spriteMaterial = new THREE.SpriteMaterial map: texture
+                spriteMaterial = new THREE.SpriteMaterial
+                    map     : texture
+                    opacity : opts.material.opacity
                 particle = new THREE.Sprite spriteMaterial

                 position = @aspect_ratio_scale opts.loc...
@@ -550,7 +542,7 @@
                 throw "bug -- unkown dynamic renderer type = #{@opts.renderer}"

         if opts.in_frame
-            @update_bounding_box particle
+            @updateBoundingBox particle
         @scene.add particle

     add_index_face_set: (opts) ->
@@ -611,7 +603,7 @@
         mesh.position.set 0, 0, 0

         if opts.in_frame
-            @update_bounding_box mesh
+            @updateBoundingBox mesh
         @scene.add mesh

     # always call this after adding things to the scene to make sure track
@@ -630,8 +622,8 @@
             z0 -= 1
         ###

-        min = @bounding_box.geometry.boundingBox.min.clone()
-        max = @bounding_box.geometry.boundingBox.max.clone()
+        min = @boundingBox.geometry.boundingBox.min.clone()
+        max = @boundingBox.geometry.boundingBox.max.clone()
         avg = min.clone().add(max).divideScalar(2)
         dim = max.clone().sub(min)

@@ -641,20 +633,20 @@
             d = 1.5*Math.max(dim.x, dim.y, dim.z)
             @camera.position.set avg.x+d, avg.y+d, avg.z+d/2

-        if @opts.frame_thickness isnt 0
+        if @frameOpts.thickness isnt 0
             # set the color and linewidth of the bounding box
-            @bounding_box.material.color = @opts.frame_color
-            @bounding_box.material.linewidth = @opts.frame_thickness
+            @boundingBox.material.color = @frameColor
+            @boundingBox.material.linewidth = @frameOpts.thickness

             # add the bounding box to the scene
-            @scene.add @bounding_box
-
-            if @_frame_labels?
-                for x in @_frame_labels
+            @scene.add @boundingBox
+
+            if @_frameLabels?
+                for x in @_frameLabels
                     @scene.remove x

-            if @opts.label_fontsize isnt 0
-                @_frame_labels = []
+            if @frameOpts.fontsize isnt 0
+                @_frameLabels = []

                 min.divide @opts.aspect_ratio
                 avg.divide @opts.aspect_ratio
@@ -663,45 +655,45 @@
                 format = (num) ->
                     Number(num.toFixed 2).toString()

-                frame_color = @opts.frame_color.getStyle()
-                add_label = (loc, text) =>
-                    @_frame_labels.push(
+                frameColor = [@frameColor.r, @frameColor.g, @frameColor.b]
+                addLabel = (loc, text) =>
+                    @_frameLabels.push(
                         @add_text
                             loc           : loc
                             text          : text
-                            fontsize      : @opts.label_fontsize
-                            color         : frame_color
+                            fontsize      : @frameOpts.fontsize
                             constant_size : false
                             in_frame      : false
+                            material:
+                                color   : frameColor
+                                opacity : 1
                         )

                 offset = 0.075

                 e = (max.y - min.y)*offset
-                add_label [max.x, min.y-e, min.z], format(min.z)
-                add_label [max.x, min.y-e, avg.z], "z = #{format avg.z}"
-                add_label [max.x, min.y-e, max.z], format(max.z)
+                addLabel [max.x, min.y-e, min.z], format(min.z)
+                addLabel [max.x, min.y-e, avg.z], "z = #{format avg.z}"
+                addLabel [max.x, min.y-e, max.z], format(max.z)

                 e = (max.x - min.x)*offset
-                add_label [max.x+e, min.y, min.z], format(min.y)
-                add_label [max.x+e, avg.y, min.z], "y = #{format avg.y}"
-                add_label [max.x+e, max.y, min.z], format(max.y)
+                addLabel [max.x+e, min.y, min.z], format(min.y)
+                addLabel [max.x+e, avg.y, min.z], "y = #{format avg.y}"
+                addLabel [max.x+e, max.y, min.z], format(max.y)

                 e = (max.y - min.y)*offset
-                add_label [max.x, max.y+e, min.z], format(max.x)
-                add_label [avg.x, max.y+e, min.z], "x = #{format avg.x}"
-                add_label [min.x, max.y+e, min.z], format(min.x)
+                addLabel [max.x, max.y+e, min.z], format(max.x)
+                addLabel [avg.x, max.y+e, min.z], "x = #{format avg.x}"
+                addLabel [min.x, max.y+e, min.z], format(min.x)

         @camera.lookAt @_center
         if @controls?
             @controls.target = @_center
-        @render_scene()

     add_obj: (opts) ->
         opts = defaults opts,
             obj       : required
             wireframe : undefined
-            set_frame : undefined

         switch opts.obj.type
             when 'group'
@@ -730,8 +722,6 @@
             else
                 console.log "ERROR: bad object type #{opts.obj.type}"

-        @render_scene true
-
     animate: (opts = {}) ->
         opts = defaults opts,
             fps       : undefined
@@ -776,8 +766,13 @@
             f()

     render_scene: (force = false) ->
-        if @renderer_type is 'static'
-            console.log 'render static -- todo'
+        if @renderer_type isnt 'dynamic'
+            # if we don't have the renderer, swap it in, make a static image,
+            # then give it back to whoever had it.
+            owner = _scene_using_renderer
+            @set_dynamic_renderer()
+            @set_static_renderer()
+            owner?.set_dynamic_renderer()
             return

         if not @camera?


Log client error stack on server

Log client error stack on server

--- server/controllers/activity.coffee
+++ server/controllers/activity.coffee
@@ -67,6 +67,7 @@
                     error: true
         when 'error'
             log.error activity.data
+            log.error activity.data.error?.stack
             res.send null
         else
             res.status(400).send


Rename: iteration -> iteratee

Rename: iteration -> iteratee

--- source/stopListening.coffee
+++ source/stopListening.coffee
@@ -23,7 +23,7 @@
       decrementListeningCount(pub, sub, n / 3) if n > 0
     return

-  stopListening__Everything__Iteration = (pub, sub) ->
+  stopListening__Everything__Iteratee = (pub, sub) ->
     ps    = pub._ps
     n     = 0
     for event, entries of ps when entries?
@@ -38,7 +38,7 @@

   stopListening__Everything = (sub) ->
     for oid, pair of sub._psTo
-      stopListening__Everything__Iteration(pair[0], sub)
+      stopListening__Everything__Iteratee(pair[0], sub)
     return

   stopListening__EventString = (pub, sub, events, callback) ->
@@ -48,12 +48,12 @@
     while ++i <= l
       if i is l or events[i] is ' '
         if j > 0
-          stopListening__EventString__Iteration(pub, sub, events[i - j...i], callback)
+          stopListening__EventString__Iteratee(pub, sub, events[i - j...i], callback)
           j = 0
       else ++j
     return

-  stopListening__EventString__Iteration = (pub, sub, event, callback) ->
+  stopListening__EventString__Iteratee = (pub, sub, event, callback) ->
     for oid, pair of sub._psTo when !pub? or pair[0] is pub
       stopListening__Base(pair[0], sub, event, callback)
     return
@@ -82,7 +82,5 @@

       else
         stopListening__AnyEvent(object, this, resolveCallback(this, callback))
-
     this
-
   return


bug fix. internal utilities was overwrited

--- src/mixins.coffee
+++ src/mixins.coffee
@@ -2,11 +2,11 @@
   _trampolines: (procedures) ->
     trampolines = {}
     for own name, proc of procedures
-      unless name.match /^_/i
+      if name.match /^_/i
+        trampolines[name] = proc
+      else
         trampolines["_#{name}"] = proc
-        trampolines[name]       = util.trampoline proc
-      else
-        trampolines[name] = proc
+        trampolines[name] = pp.trampoline proc
     trampolines

   _iteratorMixin: (mixinName, arrayIterator, hashIterator) ->


Moved setOwner out of object to view.

Moved setOwner out of object to view.

--- src/object.coffee
+++ src/object.coffee
@@ -1,3 +1,6 @@
+# ## LRSObject
+# Base object class. Provides get/set methods and events.
+
 class LRSObject

 	@isObject: true
@@ -48,9 +51,6 @@
 	set: (name, value) ->
 		@[name] = value

-	setOwner: (@owner) ->
-		@owner
-
 if module?
 	exports.LRSObject = LRSObject
 else


Don't draw our hacked zero width markers when they are offscreen

--- services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee
+++ services/web/public/coffee/ide/editor/directives/aceEditor/track-changes/TrackChangesManager.coffee
@@ -315,15 +315,14 @@
 			ace_range = new Range(position.row, position.column, position.row, position.column)
 			# Our delete marker is zero characters wide, but Ace doesn't draw ranges
 			# that are empty. So we monkey patch the range to tell Ace it's not empty.
+			# We do want to claim to be empty if we're off screen after clipping rows though.
 			# This is the code we need to trick:
 			#   var range = marker.range.clipRows(config.firstRow, config.lastRow);
 			#   if (range.isEmpty()) continue;
-			_clipRows = ace_range.clipRows
-			ace_range.clipRows = (args...) ->
-				range = _clipRows.apply(ace_range, args)
-				range.isEmpty = () ->
-					false
-				return range
+			ace_range.clipRows = (first_row, last_row) ->
+				@isEmpty = () ->
+					first_row > @end.row or last_row < @start.row
+				return @
 			return ace_range

 		_createCalloutMarker: (position, klass) ->


Add *.pyc to list of ignored file types

Add *.pyc to list of ignored file types

--- atom/config.cson
+++ atom/config.cson
@@ -13,6 +13,7 @@
     'eol': ' '
   'normalizeIndentOnPaste': false
   'showInvisibles': true
+  'scrollPastEnd': true
 'core':
   'projectHome': '/Volumes/Data/Users/Lee/Source'
   'disabledPackages': [
@@ -26,6 +27,7 @@
     '.DS_Store'
     '.keep'
     '._*'
+    '*.pyc'
   ]
   'themes': [
     'soda-light-ui'


Clean

Clean

Former-commit-id: 685ef491ad737ae0ab9139f8f974573ec92c3741 [formerly b2e04798bf941e3aa6a56324040643a554abe622]
Former-commit-id: 635b65bcc2b9cf4460ff01094df3a36d6c3fd95c

--- atom/config.cson
+++ atom/config.cson
@@ -9,8 +9,6 @@
     fontSize: 16
     invisibles: {}
     tabLength: 4
-  "exception-reporting":
-    userId: "aa61e99b-f179-1842-bacb-cbe6b437b1ba"
   linter:
     showErrorPanel: false
   minimap:


Ignore .virtualenv directory in Atom

Ignore .virtualenv directory in Atom

--- atom/config.cson
+++ atom/config.cson
@@ -43,6 +43,7 @@
       ".sass-cache"
       ".Spotlight-V100"
       ".svn"
+      ".virtualenv"
       ".Trashes"
       "node_modules"
       "Thumbs.db"


Configure the wrap guide

Configure the wrap guide

--- atom/config.cson
+++ atom/config.cson
@@ -7,7 +7,7 @@
 'metrics':
   'userId': '51eeb6113c1ae801c9aade9b6d679aa5189ced25'
 'editor':
-  'fontFamily': 'SourceCodePro-Light'
+  'fontFamily': 'Source Code Pro'
   'preferredLineLength': 100
   'invisibles':
     'cr': '↩'
@@ -20,7 +20,9 @@
   'fontSize': 14
 'core':
   'projectHome': '/Volumes/Data/Users/Lee/Source'
-  'disabledPackages': []
+  'disabledPackages': [
+    'highlight-selected'
+  ]
   'ignoredNames': [
     '.git'
     '.svn'
@@ -31,6 +33,7 @@
     'lifted-dark-ui'
     'ya-railscasts-syntax'
   ]
+  'useReactEditor': true
 'whitespace':
   'ensureSingleTrailingNewline': true
 'auto-copyright':
@@ -44,6 +47,7 @@
   ]
 'tree-view':
   'hideIgnoredNames': true
+  'hideVcsIgnoredFiles': true
 'autosave': {}
 'remember-window':
   'storeOutsideMainConfig': true
@@ -72,3 +76,11 @@
         'tabLength': 4
       'shell':
         'tabLength': 4
+'the-closer':
+  'closeWindowTogetherWithLastTab': false
+'minimap': {}
+'wrap-guide':
+  columns: [
+    { scope: 'source.gfm', column: -1 }
+    { pattern: '\.md$', column: -1 }
+  ]


Remap Go To Matching Bracket key binding

--- atom/keymap.cson
+++ atom/keymap.cson
@@ -41,6 +41,8 @@

   # Selects all text between outer matching pair of parens/brackets
   'alt-cmd-b': 'bracket-matcher:select-inside-brackets'
+  # Place cursor at at the matching bracket for the current bracket
+  'alt-cmd-m': 'bracket-matcher:go-to-matching-bracket'

   # Resizes spaces-per-indent amount for entire document
   'ctrl-cmd-[': 'resize-indent:halve'


Use uppercase letters for shift-* key bindings

--- atom/keymap.cson
+++ atom/keymap.cson
@@ -15,7 +15,7 @@
   'alt-cmd-n': 'advanced-open-file:toggle'

   # Reveal the current project folder at the workspace level
-  'shift-cmd-r': 'application:show-project-folder-in-file-manager'
+  'shift-cmd-R': 'application:show-project-folder-in-file-manager'

 # Any text field (code editors and inputs)
 '.platform-darwin atom-text-editor':
@@ -95,13 +95,13 @@
   # Expands Emmet HTML abbreviations
   'cmd-e': 'emmet:expand-abbreviation'
   # Wraps selection in Emmet HTML abbreviation
-  'shift-cmd-e': 'emmet:wrap-with-abbreviation'
+  'shift-cmd-E': 'emmet:wrap-with-abbreviation'

   # Opens inline URL in default web browser if cursor is within URL
   'alt-cmd-o': 'link:open'

   # Opens the active document in Finder
-  'shift-cmd-r': 'tree-view:show-current-file-in-file-manager'
+  'shift-cmd-R': 'tree-view:show-current-file-in-file-manager'

   # Swaps selections
   'alt-cmd-t': 'atom-transpose:transpose'
@@ -125,7 +125,7 @@
   'ctrl-cmd-a': 'sort-lines:sort'

   # Capitalize the selected text
-  'cmd-k shift-cmd-c': 'change-case:upperFirst'
+  'cmd-k shift-cmd-C': 'change-case:upperFirst'

   # Convert the selected text to camelCase
   'cmd-k cmd-c': 'change-case:camel'
@@ -159,7 +159,7 @@
   # Add a new file or folder (these are commands already invocable via the 'a'
   # and 'shift-a' keys, respectively, but these are alternative key bindings)
   'n': 'tree-view:add-file'
-  'shift-n': 'tree-view:add-folder'
+  'shift-N': 'tree-view:add-folder'

   # Remove the selected file or directory
   'cmd-backspace': 'tree-view:remove'


Update the Run Specs keymap to Ctrl+Cmd+T

--- atom/keymap.cson
+++ atom/keymap.cson
@@ -20,4 +20,4 @@
   'alt-cmd-Z': 'pane:show-previous-item'
 '.platform-darwin':
   'ctrl-cmd-r': 'tree-view:reveal-active-file'
-  'cmd-T': 'window:run-package-specs'
+  'ctrl-cmd-t': 'window:run-package-specs'


White blocks and comment blocks draggable

--- src/parser.coffee
+++ src/parser.coffee
@@ -4,7 +4,7 @@
 # Copyright (c) 2014 Anthony Bau
 # MIT License

-define ['ice-model'], (model) ->
+define ['ice-helper', 'ice-model'], (helper, model) ->
   exports = {}

   YES = -> true
@@ -206,7 +206,7 @@
         # wrap it in a generic block automatically.
         if line.length > 0
           if (opts.wrapAtRoot and stack.length is 0) or stack[stack.length - 1]?.type is 'indent'
-            block = new model.Block 0, 'blank', false
+            block = new model.Block 0, 'blank', null, helper.ANY_DROP
             socket = new model.Socket()
             socket.handwritten = true

@@ -215,6 +215,9 @@
             head = head.append new model.TextToken line
             head = head.append socket.end
             head = head.append block.end
+
+            if block.stringify().match(/^\s*#.*$/)?
+              block.socketLevel = helper.BLOCK_ONLY

           else
             head = head.append new model.TextToken line
@@ -229,7 +232,7 @@
           # (unless there is no such text
           unless lastIndex >= mark.location.column or lastIndex >= line.length
             if (opts.wrapAtRoot and stack.length is 0) or stack[stack.length - 1]?.type is 'indent'
-              block = new model.Block 0, 'blank', false
+              block = new model.Block 0, 'blank', null, helper.ANY_DROP
               socket = new model.Socket()
               socket.handwritten = true

@@ -238,6 +241,9 @@
               head = head.append new model.TextToken(line[lastIndex...mark.location.column])
               head = head.append socket.end
               head = head.append block.end
+
+              if block.stringify().match(/^\s*#.*$/)?
+                block.socketLevel = helper.BLOCK_ONLY

             else
               head = head.append new model.TextToken(line[lastIndex...mark.location.column])


get drag working on point-tool

get drag working on point-tool

--- app/assets/javascripts/components/subject-viewer.cjsx
+++ app/assets/javascripts/components/subject-viewer.cjsx
@@ -157,7 +157,7 @@
   handleInitDrag: (e) ->
     console.log 'handleInitDrag()'
     task = @props.workflow.tasks[@props.annotation.task]
-    mark = @state.selectedMark
+    mark = @props.annotation #@state.selectedMark
     # console.log "SubjectViewer#handleInitDrag"
     MarkComponent = markingTools[task.tools[mark.tool].type]
     if MarkComponent.initMove?


remove unnecessary initial states in SubjectViewer

--- app/assets/javascripts/components/subject-viewer.cjsx
+++ app/assets/javascripts/components/subject-viewer.cjsx
@@ -18,19 +18,10 @@

   getInitialState: ->
     subjects: null
-    subject: null
-    selectedDrawingTool: null
     subjectEndpoint: endpoint
-    marks: []
-    tools: []
     loading: false
-    frame: 0
     imageWidth: 0
     imageHeight: 0
-    viewX: 0
-    viewY: 0
-    viewWidth: 0
-    viewHeight: 0
     classification: null

   componentDidMount: ->


developing autocomplete

developing autocomplete

--- app/assets/javascripts/pinkman_base/controller.coffee
+++ app/assets/javascripts/pinkman_base/controller.coffee
@@ -159,6 +159,60 @@
         callback(obj,$j,args...) if callback? and typeof callback == 'function'
       # else
       #   console.log 'mesmo valor'
+
+
+  # === Autocomplete ===
+
+  # -- Usage: autocomplete(input_name,options)
+
+  # -- Overview
+  # 1: User types something
+  # 2: Pinkman searchs through a collection and then renders the result
+  # 3: User selects a record
+  # 4: Pinkman sends the selected record to call callback
+
+  # -- Parameters & Options
+  # @autocomplete(attribute,options)
+  # options:
+    # with: [collection]
+    # pinkman collection responsible for filtering the objects
+
+    # method: [search|filter|yourCustomMethod]
+    # Search is the default. Anythings that filters by a 'query'.
+    # You can define a yourCustomMethod inside the collection model like this:
+    # yourCustomMethod: (user_query, callback) ->
+      # -- perform a selection based on user_query
+      # callback(this) if $p.isFunction(callback)
+
+    # template: [template-id] (optional)
+    # Any template that has a 'select' action
+    # If you don't provide a template, Pinkman will try "attribute-autocomplete-template" by default
+
+    # target: [target-id]
+    # Where pinkman should yield results
+    # If you don't provide a template, Pinkman will try "attribute-autocomplete" by default
+
+    # before: before function
+    # Run something before searching (you can use it for some loading animation)
+
+    # call: callback function.
+    # What to do when user selects something?
+  autocomplete: (attr_name, options) ->
+    throw 'Pinkman Autocomplete: Missing options object' unless $p.isObject(options)
+    throw 'Pinkman Autocomplete: Missing "with" collection' unless options.with?
+    throw 'Pinkman Autocomplete: Missing "call" callback function'
+    collection = options.with
+    callback = options.call
+    template = if options.template? then options.template else "#{attr_name}-autocomplete"
+    target = if options.target? then options.target else "#{attr_name}-autocomplete"
+    method = if options.method? then options.method else 'search'
+    @bind attr_name, (obj) ->
+      collection[method] obj[attr_name], (collection) ->
+        collection.render
+          template: template
+          target: target
+
+

   bottom: (callback) ->
     if $("##{@id}").length
@@ -172,7 +226,7 @@
             Pinkman._lastEndOfPage = t
             callback()
       , 50
-
+
   endBottom: () ->
     Pinkman._bottomTriggered = false


Use goToNextRowFirstColumn instead of manually changing cursor pos

--- app/assets/javascripts/player/screen_buffer.js.coffee
+++ app/assets/javascripts/player/screen_buffer.js.coffee
@@ -83,8 +83,7 @@
     i = 0
     while i < text.length
       if @cursorX >= @cols
-        @cursorY += 1
-        @cursorX = 0
+        @goToNextRowFirstColumn()

       @fill @cursorY, @cursorX, 1, @charsetModifier(text[i])
       @cursorX += 1


Stripe gateway was disabling the billing name from being submitted

Fixes #113

--- app/assets/javascripts/store/gateway/stripe.js.coffee
+++ app/assets/javascripts/store/gateway/stripe.js.coffee
@@ -7,20 +7,13 @@
   $('.continue').click ->
     $('#stripeError').hide()
     if Spree.stripePaymentMethod.is(':visible')
-      expiry_parts = $('.cardExpiry:visible').val()
-      if expiry_parts.length > 4
-        expiry_parts = expiry_parts.split("/")
-        expiry_month = expiry_parts[0].replace(/\ /g, '')
-        expiry_year = expiry_parts[1].replace(/\ /g, '')
-      else
-        expiry_month = ""
-        expiry_year = ""
+      expiration = $('.cardExpiry:visible').payment('cardExpiryVal')
       params = $.extend(
         {
           number: $('.cardNumber:visible').val(),
           cvc: $('.cardCode:visible').val(),
-          exp_month: expiry_month,
-          exp_year: expiry_year,
+          exp_month: expiration.month,
+          exp_year: expiration.year,
         },
         Spree.stripeAdditionalInfo
       )
@@ -33,12 +26,15 @@
     $('#stripeError').html(response.error.message)
     $('#stripeError').show()
   else
-    Spree.stripePaymentMethod.find("input").prop("disabled" , true);
+    Spree.stripePaymentMethod.find('#card_number, #card_expiry, #card_code').prop("disabled" , true)
     Spree.stripePaymentMethod.find(".ccType").prop("disabled", false)
     # token contains id, last4, and card type
     token = response['id'];
     # insert the token into the form so it gets submitted to the server
-    payment_method_id = Spree.stripePaymentMethod.prop('id').split("_")[2]
-    Spree.stripePaymentMethod.append("<input type='hidden' class='stripeToken' name='payment_source[" + payment_method_id + "][gateway_payment_profile_id]' value='" + token + "'/>");
+    paymentMethodId = Spree.stripePaymentMethod.prop('id').split("_")[2]
+    Spree.stripePaymentMethod.append("<input type='hidden' class='stripeToken' name='payment_source[" + paymentMethodId  + "][gateway_payment_profile_id]' value='" + token + "'/>");
+    # insert additional card info to save to the payment source
+    expiration = $('.cardExpiry:visible').payment('cardExpiryVal')
+    Spree.stripePaymentMethod.append("<input type='hidden' name='payment_source[" + paymentMethodId + "][month]' value='" + expiration.month + "'/>");
+    Spree.stripePaymentMethod.append("<input type='hidden' name='payment_source[" + paymentMethodId + "][year]' value='" + expiration.year + "'/>");
     Spree.stripePaymentMethod.parents("form").get(0).submit();
-


Fix Player sometimes visibly not changing direction.

This is also related to Player sometimes stopping and/or starting
redundantly. The bugs seem inter-related.

--- src/player.coffee
+++ src/player.coffee
@@ -125,7 +125,6 @@

       if _.isNumber(nameOrFrame)
         frame = nameOrFrame
-        return if @animations.frame is frame and !@animation?
         @animations.frame = frame
         @animation = null

@@ -193,14 +192,15 @@
         @sprite.scale.x = direction
         @physics.offset = new Phaser.Point @_xOffset(direction), @_yOffset

-      else unless (@_isTurning or @nextAction is 'stop')
-        @nextAction = 'stop' unless @velocity.x is 0
+      else if !@_isTurning and @nextAction isnt 'stop' and @velocity.x isnt 0
+        @nextAction = 'stop'
         @_isTurning = yes
         @direction = direction
         @debug 'turn:start', @velocity.x

     _updateAnimations: ->
       unless @isInMidAir() or @nextAction is 'none'
+        return if @nextAction is 'stop' and @animations.frame is 17 # Patch.
         @_changeAnimation @nextAction, @animation?.loop
         return


automatically pin if screen > 800

automatically pin if screen > 800

Signed-off-by: Pierre Tardy <bb5a2a16eebb4a48138fca46fede24b3c8d635fe@intel.com>

--- src/module/page_with_sidebar/page_with_sidebar.directive.coffee
+++ src/module/page_with_sidebar/page_with_sidebar.directive.coffee
@@ -11,14 +11,14 @@
         }

 class _glPageWithSidebar extends Controller
-    constructor: (@$scope, glMenuService, @$timeout) ->
-        @sidebarPinned = false
+    constructor: (@$scope, glMenuService, @$timeout, @$window) ->
+        @sidebarPinned = @$window.innerWidth > 800
         @groups = glMenuService.getGroups()
         @footer = glMenuService.getFooter()
         @appTitle = glMenuService.getAppTitle()
         @activeGroup = null
         @inSidebar = false
-        @sidebarActive = false
+        @sidebarActive = @sidebarPinned

     toggleGroup: (group) ->
         if @activeGroup!=group
@@ -44,4 +44,3 @@
                 @sidebarActive = false
                 @activeGroup = null
             ), 500
-


Fix StartTab default App shortcuts issues.

Instead of creating storage in constructor just create it when we need.

--- client/app/MainApp/kodingappscontroller.coffee
+++ client/app/MainApp/kodingappscontroller.coffee
@@ -64,17 +64,15 @@
     super

     @kiteController = @getSingleton('kiteController')
-    @appStorage = new AppStorage 'KodingApps', '1.0'
-
-    @appStorage.fetchStorage (storage)=>
-      if not @appStorage.getValue 'shortcuts'
-        @putDefaultShortcutsToAppStorage()

   # #
   # FETCHERS
   # #

   fetchApps:(callback)->
+
+    if KD.isLoggedIn() and not @appStorage?
+      @appStorage = new AppStorage 'KodingApps', '1.0'

     if Object.keys(@constructor.manifests).length isnt 0
       callback null, @constructor.manifests
@@ -141,12 +139,23 @@

   fetchAppsFromDb:(callback)->

-    @appStorage.fetchValue 'apps', (apps)=>
-      if apps and Object.keys(apps).length > 0
-        @constructor.manifests = apps
-        callback null, apps
+    @appStorage.fetchStorage (storage)=>
+
+      apps = @appStorage.getValue 'apps'
+      shortcuts = @appStorage.getValue 'shortcuts'
+
+      justFetchApps = =>
+        if apps and Object.keys(apps).length > 0
+          @constructor.manifests = apps
+          callback null, apps
+        else
+          callback new Error "There are no apps in the app storage."
+
+      if not shortcuts
+        @putDefaultShortcutsToAppStorage =>
+          justFetchApps()
       else
-        callback new Error "There are no apps in the app storage."
+        justFetchApps()

   fetchCompiledApp:(manifest, callback)->

@@ -177,7 +186,7 @@
       @appStorage.setValue 'shortcuts', shortcuts, (err)=>
         callback err

-  putDefaultShortcutsToAppStorage:->
+  putDefaultShortcutsToAppStorage:(callback)->

     shortcuts       =
       Ace           :
@@ -212,7 +221,8 @@
         description : 'Image Editor'
         author      : 'Autodesk'

-    @appStorage.setValue 'shortcuts', shortcuts
+    @appStorage.reset()
+    @appStorage.setValue 'shortcuts', shortcuts, callback

   putAppsToAppStorage:(apps)->


Put apps to appstorages when its really needed.

--- client/app/MainApp/kodingappscontroller.coffee
+++ client/app/MainApp/kodingappscontroller.coffee
@@ -54,7 +54,8 @@

     @watcher.on "ManifestHasChanged", (app, change)=>
       @fetchAppFromFs app, =>
-        @emit "UpdateAppData", app
+        @putAppsToAppStorage null, =>
+          @emit "UpdateAppData", app

   getAppPath:(manifest, escaped=no)->

@@ -107,7 +108,7 @@
         warn "Manifest file is broken:", e
         return cb null

-      @putAppsToAppStorage manifests
+      @constructor.manifests = manifests
       cb null

   fetchAppsFromFsHelper:(apps, callback)->
@@ -131,6 +132,7 @@
       else
         apps = @filterAppsFromFileList files
         @fetchAppsFromFsHelper apps, (result)=>
+          @putAppsToAppStorage()
           for callback in @_fetchQueue
             callback null, @getManifests()
           @_fetchQueue = []


Sanity check for approve status before app install

--- client/app/MainApp/kodingappscontroller.coffee
+++ client/app/MainApp/kodingappscontroller.coffee
@@ -408,30 +408,35 @@
           callback? msg : "App is already installed!"
         else
           log "installing the app: #{app.title}"
-          app.fetchCreator (err, acc)=>
-            # log err, acc, ">>>>"
-            if err
-              callback? err
-            else
-              options =
-                toDo          : "installApp"
-                withArgs      :
-                  owner       : acc.profile.nickname
-                  appPath     : getAppPath app.manifest
-                  appName     : app.manifest.name
-                  version     : version
-              log "asking kite to install", options
-              @kiteController.run options, (err, res)=>
-                log "kite response", err, res
-                if err then warn err
-                else
-                  app.install (err)=>
-                    log err if err
-                    log callback
-                    # This doesnt work :#
-                    appManager.openApplication "StartTab"
-                    @refreshApps()
-                    # callback?()
+          if not app.approved
+            err = "This app is not approved, installation cancelled."
+            log err
+            callback? err
+          else
+            app.fetchCreator (err, acc)=>
+              # log err, acc, ">>>>"
+              if err
+                callback? err
+              else
+                options =
+                  toDo          : "installApp"
+                  withArgs      :
+                    owner       : acc.profile.nickname
+                    appPath     : getAppPath app.manifest
+                    appName     : app.manifest.name
+                    version     : version
+                log "asking kite to install", options
+                @kiteController.run options, (err, res)=>
+                  log "kite response", err, res
+                  if err then warn err
+                  else
+                    app.install (err)=>
+                      log err if err
+                      log callback
+                      # This doesnt work :#
+                      appManager.openApplication "StartTab"
+                      @refreshApps()
+                      # callback?()

   # #
   # MAKE NEW APP


rename pos or id to posid

rename pos or id to posid

--- src/puzzle.coffee
+++ src/puzzle.coffee
@@ -574,8 +574,8 @@
     toString.call(obj) == '[object Array]'

-toPos = (posOrID) ->
-    return if isArray(posOrID) then posOrID else @position[posOrID]
+toPos = (posid) ->
+    return if isArray(posid) then posid else @position[posid]

 class Sliding
@@ -665,7 +665,7 @@

         return @

-    solvable: (pos) ->
+    solvable: ->
         [erow, _] = @position[@emptyID]
         [inversions, _] = countInversions(@grid, @emptyID)
         return solvable(inversions, @rows, @cols, erow + 1)
@@ -724,10 +724,10 @@

         return @

-    slide: (pos, handler) ->
-        if not @slidable(pos)
+    slide: (posid, handler) ->
+        if not @slidable(posid)
             return @
-        [row, col] = toPos.call(@, pos)
+        [row, col] = toPos.call(@, posid)
         [erow, ecol] = @position[@emptyID]
         if row == erow
             handler?.call(@, @grid[row][c], @emptyID)
@@ -737,8 +737,8 @@
             @swap([r, col], @emptyID) for r in [erow..row]
         return @

-    slidable: (pos) ->
-        [row, col] = toPos.call(@, pos)
+    slidable: (posid) ->
+        [row, col] = toPos.call(@, posid)
         [erow, ecol] = @position[@emptyID]
         return (0 <= row < @rows and 0 <= col < @cols) and
             not (row == erow and col == ecol) and


Sidebar: render stacks header even there is no stack yet

--- client/app/lib/components/sidebar/index.coffee
+++ client/app/lib/components/sidebar/index.coffee
@@ -122,8 +122,6 @@

   renderStacks: ->

-    return null  unless @state.stacks?.size
-
     <SidebarStackHeaderSection>
       {@divideStacks()}
     </SidebarStackHeaderSection>


app/flux: add private stack template getters

--- client/app/lib/flux/environment/getters.coffee
+++ client/app/lib/flux/environment/getters.coffee
@@ -24,6 +24,7 @@
 DifferentStackResourcesStore      = ['DifferentStackResourcesStore']
 ActiveStackStore                  = ['ActiveStackStore']
 TeamStackTemplatesStore           = ['TeamStackTemplatesStore']
+PrivateStackTemplatesStore        = ['PrivateStackTemplatesStore']

 workspacesWithChannels = [
@@ -141,8 +142,24 @@
         .set 'isDefault', template.get('_id') in (getGroup().stackTemplates or [])
 ]

+privateStackTemplates = [
+  PrivateStackTemplatesStore
+  stacks
+  (templates, _stacks) ->
+    baseStackIds = _stacks.toList().toArray().map (s) -> s.get 'baseStackId'
+    templates.map (template) ->
+      template
+        .set 'inUse', template.get('_id') in baseStackIds
+        .set 'isDefault', template.get('_id') in (getGroup().stackTemplates or [])
+]
+
 inUseTeamStackTemplates = [
   teamStackTemplates
+  (templates) -> templates.filter (t) -> t.get('inUse')
+]
+
+inUsePrivateStackTemplates = [
+  privateStackTemplates
   (templates) -> templates.filter (t) -> t.get('inUse')
 ]

@@ -164,5 +181,7 @@
   activeLeavingSharedMachineId: ActiveLeavingSharedMachineIdStore
   differentStackResourcesStore : DifferentStackResourcesStore
   teamStackTemplates
+  privateStackTemplates
   inUseTeamStackTemplates
+  inUsePrivateStackTemplates
 }


ParticipantView: Update button title and emit event.

--- client/ide/views/modals/participantview.coffee
+++ client/ide/views/modals/participantview.coffee
@@ -46,10 +46,13 @@
           cssClass : 'info'
           partial  : "and #{missing} more"

-      @addSubView watchButton = new KDButtonView
+      @addSubView @watchButton = new KDButtonView
         title      : 'Watch'
         cssClass   : 'solid compact green'
-        callback   : => @emit 'ParticipantWatchRequested'
+        callback   : =>
+          title = if @watchButton.getTitle() is 'Watching' then 'Watch' else 'Watching'
+          @watchButton.setTitle title
+          @emit 'ParticipantWatchRequested', participant

     else
       @addSubView new KDCustomHTMLView


Use input component when editing a decimal

--- client/lanes/components/grid/EditingMixin.cjsx
+++ client/lanes/components/grid/EditingMixin.cjsx
@@ -1,9 +1,7 @@
 Lanes.Components.Grid.EditingMixin = {

     propTypes:
-        #topOffset: React.PropTypes.number.isRequired
         rowIndex:  React.PropTypes.number.isRequired
-#        rowHeight: React.PropTypes.number.isRequired
         onSave:    React.PropTypes.func.isRequired
         onCancel:  React.PropTypes.func.isRequired
         model:     Lanes.PropTypes.State.isRequired
@@ -17,18 +15,16 @@
         ])
     editorTypes:
         text: (props) ->
-            <input type="text"
-                name={props.field.id}
-                value={props.value}
+            <input type="text" {...props}
                 onChange={_.partial(@onFieldChange, _, props.field)} />
+        bigdec: (props) ->
+            <LC.Input onlyNumeric inputOnly {...props} />
         date: (props) ->
-            <LC.DateTime inputOnly step={15}
-                model={props.model} name={props.field.id}
+            <LC.DateTime {...props} inputOnly step={15}
                 onChange={_.partial(@onDateFieldChange, _, props.field)} />

     listenNetworkEvents: true
     getDefaultProps: -> editors: {}
-    #topOffset:       -> @props.topOffset + (@props.rowIndex * @props.rowHeight)

     renderControls: ->
         if @props.allowDelete
@@ -50,10 +46,7 @@
         </div>

     getFieldValue: (field) ->
-        if field.format
-            field.format(@props.model[field.id], @props.model)
-        else
-            @props.model[field.id]
+        @props.model[field.id]

     onDateFieldChange: (date, field) ->
         @props.model[field.id] = date
@@ -77,9 +70,14 @@

     renderField: (field, index) ->
         control = if field.editable
-            props = _.extend( {index, field, value: @getFieldValue(field)},
-                _.pick(@props, 'model', 'query', 'rowIndex')
-            )
+            props = _.extend( {
+                index, field,
+                props: @props,
+                name: field.id,
+                value: @getFieldValue(field)
+
+            }, _.pick(@props, 'model', 'query', 'rowIndex') )
+
             (@props.editors[field.id] || @editorTypes[field.type] || @editorTypes['text']).call(this, props)
         else
             <span>{@getFieldValue(field)}</span>
@@ -110,8 +108,9 @@

     deleteRecord: ->
         if @shouldPerformSync()
-            @props.model.destroy().then (model) => @props.onSave(model)
+            @props.model.destroy().then (model) =>
+                @props.onSave(model, true)
         else
-            @props.onSave(@props.model)
+            @props.onSave(@props.model, true)

 }


Tests: Messages: Improve startConversation helper.

--- client/test/lib/helpers/messageshelpers.coffee
+++ client/test/lib/helpers/messageshelpers.coffee
@@ -55,18 +55,18 @@
               browser.waitForElementPresent "#{sidebarTextSelector} [href='/#{user.username}']", 20000

-    browser.element 'css selector', messageSelector, (result) =>
-      if result.status is 0
-        console.log " ✔ A message thread with the same user is already exists. Ending test."
-        return yes
-      else
-        if purpose
-          browser.element 'css selector', purposeSelector, (result) =>
-            if result.status is 0
-              console.log " ✔ A message thread with the same purpose is already exists. Ending test."
-              return yes
-            else
-              sendMessage browser, users, message, purpose
+    if purpose
+      browser.element 'css selector', purposeSelector, (result) =>
+        if result.status is 0
+          console.log " ✔ A message thread with the same purpose is already exists. Ending test."
+          return yes
+        else
+          sendMessage browser, users, message, purpose
+    else
+      browser.element 'css selector', messageSelector, (result) =>
+        if result.status is 0
+          console.log " ✔ A message thread with the same user is already exists. Ending test."
+          return yes
         else
           sendMessage browser, users, message


[TASK] Add class method to get viewport information

--- src/resimg.coffee
+++ src/resimg.coffee
@@ -156,14 +156,19 @@
     return if @images.length == 0

     # Gather some information about the users device
-    viewportWidth    = window.innerWidth  || document.documentElement.clientWidth  || document.body.clientWidth
-    viewportHeight   = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
-    deviceHasHdpi    = if window.devicePixelRatio and window.devicePixelRatio >= 1.2 then 1 else 0
-    deviceIsPortrait = viewportHeight > viewportWidth
+    [viewportWidth, viewportHeight, deviceHasHdpi, deviceIsPortrait] = @getViewportInfo()

     # Process all the images found on the page
     for image in @images
       image.getImageSrc(viewportWidth, deviceHasHdpi, deviceIsPortrait)
+
+  getViewportInfo: () ->
+    viewportWidth    = window.innerWidth  || document.documentElement.clientWidth  || document.body.clientWidth
+    viewportHeight   = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
+    deviceHasHdpi    = if (window.devicePixelRatio and window.devicePixelRatio >= 1.2) then true else false
+    deviceIsPortrait = viewportHeight > viewportWidth
+
+    return [viewportHeight, viewportHeight, deviceHasHdpi, deviceIsPortrait]

 debounce = (func, wait, immediate) ->


Remove internal binders. These should be implemented as subclasses on Rivets.Binder and should be kept to a minimum.

--- src/rivets.coffee
+++ src/rivets.coffee
@@ -24,7 +24,7 @@
   # containing view, the DOM node, the type of binding, the model object and the
   # keypath at which to listen for changes.
   constructor: (@view, @el, @type, @key, @keypath, @options = {}) ->
-    unless @binder = Rivets.internalBinders[@type] or @view.binders[type]
+    unless @binder = @view.binders[type]
       for identifier, value of @view.binders
         if identifier isnt '*' and identifier.indexOf('*') isnt -1
           regexp = new RegExp "^#{identifier.replace('*', '.+')}$"
@@ -642,15 +642,6 @@
     else
       el.removeAttribute @type

-# Rivets.internalBinders
-# ----------------------
-
-# Contextually sensitive binders that are used outside of the standard attribute
-# bindings. Put here for fast lookups and to prevent them from being overridden.
-Rivets.internalBinders =
-  textNode: (node, value) ->
-    node.data = value ? ''
-
 # Rivets.components
 # -----------------


capitalize artist to be consistent

capitalize artist to be consistent

--- apps/artist/client/index.coffee
+++ apps/artist/client/index.coffee
@@ -38,7 +38,7 @@
     @setupHeader()

     # Track pageview
-    analytics.track.impression 'artist page', { id: @model.id }
+    analytics.track.impression 'Artist page', { id: @model.id }

   setupHeader: ->
     @setupBlurb()


KDContentEditableView: fix drop event data getter

--- components/inputs/KDContentEditableView.coffee
+++ components/inputs/KDContentEditableView.coffee
@@ -119,7 +119,7 @@

   drop: (event) ->
     event.preventDefault()
-    text = @getClipboardTextNode event.originalEvent.dataTransfer
+    text = event.originalEvent.dataTransfer.getData "text/plain"
     {originalEvent: {clientX, clientY}} = event

     if @getValue() is ""


Map fix

Map fix

--- components/modules/Home/includes/js/map.coffee
+++ components/modules/Home/includes/js/map.coffee
@@ -34,7 +34,7 @@
 					new ymaps.Placemark(
 						[event.lat, event.lng]
 						{
-							hintContent	: event.category_name
+							hintContent	: category_name
 						}
 						{
 							iconLayout			: 'default#image'


fix build

fix build

--- components/profile_header/test/template.coffee
+++ components/profile_header/test/template.coffee
@@ -35,7 +35,6 @@
       $ = cheerio.load render('template')({ profile: @profile })
       $('.profile-badge-name').text().should.equal @profile.displayName()
       $('.profile-followers').text().should.equal @profile.formatFollowText()
-      $('.follow-button').length.should.equal 0
       $('.edit-profile').length.should.equal 1

   describe 'with an icon', ->


display low-key cta bar to logged out users from external source

--- apps/artist/client/cta.coffee
+++ apps/artist/client/cta.coffee
@@ -1,4 +1,4 @@
-{ MEDIUM, CURRENT_USER, SHOW_ARTIST_CTA } = require('sharify').data
+{ REFERRER, APP_URL, CURRENT_USER, SHOW_ARTIST_CTA } = require('sharify').data
 { contains } = require 'underscore'
 CTABarView = require '../../../components/cta_bar/view.coffee'
 ArtistPageCTAView = require '../../../components/artist_page_cta/view.coffee'
@@ -12,24 +12,25 @@
     $('body').append artistPageCTAView.render().$el
     return

-  return unless MEDIUM is 'search' and not CURRENT_USER?
+  # When user is logged-out and the referrer is an external source
+  # show cta bar
+  if !CURRENT_USER and !REFERRER?.includes(APP_URL)
+    name = 'artist_cta'
+    ctaHeadline = "Get updates on new shows and works by #{artist.get('name')}."
+    modalCopy = "Get the latest new shows and works by #{artist.get('name')}."

-  name = 'artist_cta'
-  ctaHeadline = "Get updates on new shows and works by #{artist.get('name')}."
-  modalCopy = "Get the latest new shows and works by #{artist.get('name')}."
+    ctaBarView = new CTABarView
+      headline: ctaHeadline
+      name: name
+      mode: ''
+      persist: true
+      modalOptions:
+        copy: modalCopy
+        destination: "#{artist.href()}/follow"

-  ctaBarView = new CTABarView
-    headline: ctaHeadline
-    name: name
-    mode: ''
-    persist: true
-    modalOptions:
-      copy: modalCopy
-      destination: "#{artist.href()}/follow"
+    unless ctaBarView.previouslyDismissed()
+      $('body').append ctaBarView.render().$el

-  unless ctaBarView.previouslyDismissed()
-    $('body').append ctaBarView.render().$el
-
-    $('#artwork-section').waypoint (direction) ->
-      ctaBarView.transitionIn() if direction is 'down'
-    , { offset: 'bottom-in-view' }
+      $('#artwork-section').waypoint (direction) ->
+        ctaBarView.transitionIn() if direction is 'down'
+      , { offset: 'bottom-in-view' }


sorting correctly, note about momentjs

sorting correctly, note about momentjs

--- event-box.coffee
+++ event-box.coffee
@@ -13,7 +13,7 @@
 				results.push(event)
 		# sort results
 		results.sort (a, b) ->
-			b.datetime.getTime() - a.datetime.getTime()
+			b.datetime.getTime() - a.datetime.getTime()
 		React.createElement "div", {id: "events", className: "col-sm-6"},
 			React.createElement("h1", null, "Events")
 			React.createElement(InputBox, {searchString: this.props.search, onSearchChange: this.handleSearchChange})
@@ -50,4 +50,4 @@
 				this.props.resultsList.map (result) =>
 					React.createElement "tr", {onClick: this.handleEventSelect.bind(this, result)},
 						React.createElement("td", null, result.description)
-						React.createElement("td", null, result.datetime.toDateString()) # this is wherer
+						React.createElement("td", null, result.datetime.toDateString()) # this is where we would put moment.js conversion/formatting


fix(composer): fix backwards typing text

--- internal_packages/composer/lib/contenteditable-component.cjsx
+++ internal_packages/composer/lib/contenteditable-component.cjsx
@@ -79,6 +79,7 @@
     @_editableNode().focus() if @isMounted()

   _onInput: (event) ->
+    @_ignoreSelectionRestoration = false
     @_editableNode().normalize()
     @_setNewSelectionState()
     html = @_unapplyHTMLDisplayFilters(@_editableNode().innerHTML)


Fix variable name

Fix variable name

--- lib/dashing-contrib/assets/widgets/switcher/switcher.coffee
+++ lib/dashing-contrib/assets/widgets/switcher/switcher.coffee
@@ -75,7 +75,7 @@
 # Supports optional switcher interval, defaults to 5sec
 class WidgetSwitcher
   constructor: (@elements) ->
-    @$elements = $(elements)
+    @$elements = $(@elements)

   start: (interval=5000) ->
     self = @


fixed cloning bug introduced by defining all attributes in constructor

--- evolution.coffee
+++ evolution.coffee
@@ -689,6 +689,7 @@
   f(x) for x in ar

 dup = (obj) ->
+  return null unless obj?
   if ( obj instanceof Array )
     map obj, (o) -> dup o
   else if ( typeof obj == 'object' )
@@ -805,6 +806,7 @@
     @type      = Thing
     @others    = {}
     @outlined  = false
+    @marges    = null
     unless @dontAdd
       uni = @universe
       uni.change = true
@@ -869,7 +871,7 @@
   margins: ->
     m = @marges ?= []
     return m if m.length
-    fi = PI * @visualAngle() / 2
+    fi = PI * @visualAngle()
     t1 = @angle - fi
     t1 = @universe.tp( sin(t1), cos(t1) )
     t2 = @angle + fi
@@ -904,12 +906,20 @@
       return if isAbove then QT else 3 * QT
     if @y == t.y
       return if isBefore then 0 else PI
-    theta = PI * acos abs( @x - t.x ) / @dist(t)
-    theta = if isAbove
-      if isBefore then theta else PI - theta
+    theta = degrees acos abs( @x - t.x ) / @dist(t)
+    if isAbove
+      if isBefore then theta else 180 - theta
     else
-      if isBefore then TAU - theta else PI + theta
-    degrees theta
+      if isBefore then 360 - theta else 180 + theta
+  relativeGeometry: (t) ->
+    offset = @others[t.id]
+    if offset
+      distance: @universe.geoData[offset]
+      sine:     @universe.geoData[offset + 1]
+      cosine:   @universe.geoData[offset + 2]
+      segment:  @universe.geoData[offset + 3]
+    else
+      'beyond maximum distance considered'

 class Stone extends Thing
   constructor: ( location, options = {} ) ->
@@ -1293,8 +1303,21 @@
     for t in heard
       t.outline()
       t.draw()
-  logGeometry: (t) ->
-    h = @dist t
+  relativeGeometry: (t) ->
+    data = super(t)
+    [ t1, t2 ] = @margins()
+    data.auditoryScope = @auditoryRange()
+    data.visualScope =
+      range: @visualRange()
+      left:
+        sine:    @universe.geoData[ t1 + 1 ]
+        cosine:  @universe.geoData[ t1 + 2 ]
+        segment: @universe.geoData[ t1 + 3 ]
+      right:
+        sine:    @universe.geoData[ t2 + 1 ]
+        cosine:  @universe.geoData[ t2 + 2 ]
+        segment: @universe.geoData[ t2 + 3 ]
+    data

 class Herbivore extends Animal


inital date fix

inital date fix

--- src/components/task-plan/builder.cjsx
+++ src/components/task-plan/builder.cjsx
@@ -57,12 +57,12 @@
       tasking

   getOpensAtDefault: ->
-    moment(TimeStore.getNow()).add(1, 'day').format(TutorDateFormat)
+    moment(TimeStore.getNow()).add(1, 'day').toDate()

   getQueriedOpensAt: ->
     {opens_at} = @getQuery() # attempt to read the open date from query params
     isNewPlan = TaskPlanStore.isNew(@props.id)
-    opensAt = if opens_at and isNewPlan then moment(opens_at).format(TutorDateFormat)
+    opensAt = if opens_at and isNewPlan then moment(opens_at).toDate()
     if not opensAt
       # default open date is tomorrow
       opensAt = @getOpensAtDefault()
@@ -71,14 +71,14 @@
     dueAt = @getQueriedDueAt()
     if dueAt? and moment(dueAt).isSame(opensAt, 'day')
       # make open date today if default due date is tomorrow
-      opensAt = moment(TimeStore.getNow()).format(TutorDateFormat)
+      opensAt = moment(TimeStore.getNow()).toDate()

     opensAt

   getQueriedDueAt: ->
     {due_at} = @getQuery() # attempt to read the due date from query params
     isNewPlan = TaskPlanStore.isNew(@props.id)
-    dueAt = if due_at and isNewPlan then moment(due_at).format(TutorDateFormat)
+    dueAt = if due_at and isNewPlan then moment(due_at).toDate()

   # Copies the available periods from the course store and sets
   # them to open at the default start date


SNAKEZ IN YO BOWZERRRR

SNAKEZ IN YO BOWZERRRR

--- source/coffeescripts/kernel.coffee
+++ source/coffeescripts/kernel.coffee
@@ -1,7 +1,33 @@
 define ->
-  setupKernel = ->
-    tick = (dir)->
-      console.log( "tick: #{dir}" )
+
+  nextHead = (currentHead, dir)->
+   switch dir
+     when 'left' then [currentHead[0]-1,currentHead[1]]
+     when 'right' then [currentHead[0]+1,currentHead[1]]
+     when 'up' then [currentHead[0],currentHead[1]-1]
+     when 'down' then [currentHead[0],currentHead[1]+1]
+
+
+  renderSnake = (snake,board)->
+    board.clear()
+    _.each snake, ([x,y])->
+      board.paint(x,y)
+
+  updateSnake = (snake,dir)->
+    head = snake[snake.length-1]
+    newHead = nextHead( head, dir )
+    snake.push( newHead )
+    snake.shift()
+
+  setupKernel = (board)->
+    snake = [[13,33],[14,33],[15,33]]
+    dir = 'right'
+
+
+    tick = (newDir)->
+      dir = newDir unless newDir == 'none'
+      updateSnake(snake,dir)
+      renderSnake(snake,board)

     {
       tick: tick


adding kostas to the list

adding kostas to the list

--- scripts/offshore.coffee
+++ scripts/offshore.coffee
@@ -60,7 +60,9 @@
     msg.send msg.random chrisExcuse

   robot.respond /name traitors/i, (msg)->
-    msg.send ":crown: Marek :crown:, Thomas, Prem :prem:, Chris :chris:, Sachin :sachin:"
+    msg.send ":crown: Marek :crown:, Thomas, Prem :prem:, Chris :chris:, Sachin :sachin:, Kostas :kostas: \n
+    Ah... the sweet sweet joy of a dark alley, where a cold steal sends best regards from the offshore team. \n
+    We don't forget. Never forget..."

   robot.respond /ship CBRIS/i, (msg)->
     msg.send "https://raw.githubusercontent.com/sagasu/offshore-team/master/img/offshoreZillaRunningInChrome.jpg"


Use high-quality images in multiple list

--- app/assets/javascripts/uploadcare/widget/tabs/preview-tab-multiple.coffee
+++ app/assets/javascripts/uploadcare/widget/tabs/preview-tab-multiple.coffee
@@ -117,17 +117,9 @@
       @__updateFileInfo(fileEl, info)

       if info.isImage
-        cdnURL = "#{info.cdnUrl}-/quality/lightest/" +
-          if @settings.imagesOnly
-          then "-/preview/54x54/"
-          else "-/preview/54x54/"
-        cdnURL_2x = "#{info.cdnUrl}-/quality/lightest/" +
-          if @settings.imagesOnly
-          then "-/preview/108x108/"
-          else "-/preview/108x108/"
+        cdnURL = "#{info.cdnUrl}-/quality/lightest/-/preview/108x108/"
         filePreview = $('<img>')
           .attr('src', cdnURL)
-          .attr('srcset', cdnURL_2x + ' 2x')
           .addClass('uploadcare--file__icon')
       else
         filePreview = $("<svg width='32' height='32'><use xlink:href='#uploadcare--icon-file'/></svg>")


Fix: don't attempt to use current_entity when not signed in

--- assets/javascripts/background_mentions_pool.js.coffee
+++ assets/javascripts/background_mentions_pool.js.coffee
@@ -1,7 +1,7 @@
 class BackgroundMentionsPool
   sep: '__SEP__'
   constructor: ->
-    @entity = TentStatus.config.current_entity.toStringWithoutSchemePort()
+    return unless @entity = TentStatus.config.current_entity?.toStringWithoutSchemePort()
     @cache_key = {
       since_id: "#{@entity}#{@sep}mentions_since_id"
       since_id_entity: "#{@entity}#{@sep}mentions_since_id_entity"


Add total housing units

Add total housing units


adds a spinner and block the user while the rates are being fetched

--- app/assets/javascripts/controllers/expense_report_form_controller.js.coffee
+++ app/assets/javascripts/controllers/expense_report_form_controller.js.coffee
@@ -5,6 +5,26 @@
   $scope.addExpense = ->
     $scope.newExpenseReport.addExpense()

+  $scope.updateRate = (expense) ->
+    $scope.showSpinner()
+    $scope.newExpenseReport.updateRate(expense, $scope.hideSpinner)
+
+  $scope.showSpinner = ->
+    $("body").spin()
+    $("body").block
+      fadeIn: 250
+      message: ''
+      overlayCSS:
+        opacity: 0.35
+
+  $scope.hideSpinner = ->
+    $("body").unblock
+      fadeOut: 250
+      onUnblock: ->
+        $("body").spin(false)
+
   $scope.$watch("newExpenseReport.endTime - newExpenseReport.startTime", ->
     $scope.newExpenseReport.updatePerDiumExpense()
+    for exp in $scope.newExpenseReport.expenses
+      $scope.updateRate(exp) if $scope.newExpenseReport.endTime and $scope.newExpenseReport.startTime
   )


Add removeTrailingWhitespace true on GFM specs to ensure it always works

--- spec/whitespace-spec.coffee
+++ spec/whitespace-spec.coffee
@@ -206,6 +206,7 @@
   describe "GFM whitespace trimming", ->
     describe 'when keepMarkdownLineBreakWhitespace is true', ->
       beforeEach ->
+        atom.config.set("whitespace.removeTrailingWhitespace", true)
         atom.config.set("whitespace.ignoreWhitespaceOnCurrentLine", false)
         atom.config.set("whitespace.keepMarkdownLineBreakWhitespace", true)


changed concept keyword

changed concept keyword

--- scripts/snomedct.coffee
+++ scripts/snomedct.coffee
@@ -10,7 +10,7 @@
 # HUBOT_SLACK_TOKEN="..."
 #
 # Commands:
-#   @sctbot concept <search term> - search SNOMED CT for a given term
+#   @sctbot sctconcept <search term> - search SNOMED CT for a given term
 #   @sctbot sctid <id> - return the term for the give SNOMED CT identifier
 #
 # Notes:
@@ -52,7 +52,7 @@

-  robot.hear /concept (.*)/i, (res) ->
+  robot.hear /sctconcept (.*)/i, (res) ->
     concept = res.match[1]
     robot.http("http://browser.ihtsdotools.org/api/snomed/en-edition/v20160731/descriptions?query=#{concept}&statusFilter=activeOnly")
     .header('Accept', 'application/json')


Clean up code

Clean up code

--- batsignal.coffee
+++ batsignal.coffee
@@ -1,18 +1,19 @@
 messages = new Meteor.Collection 'messages'
+
+allMessages = messages.find {}
+
+latestMessage = messages.find {}, { sort: { $natural: -1 }, limit: 1 }

 messages.isValid = (message) -> typeof message is 'string' and message.length < 50

 messages.add = (message) -> if @isValid message then @insert message: message, isRead: false

 if Meteor.is_server
-	Meteor.publish 'messages', ->
-		messages.find {},
-			sort: $natural: -1
-			limit: 1
+	Meteor.publish 'messages', -> latestMessage
 else
 	Meteor.subscribe 'messages'

-	messages.find({}).observe added: (msg) -> if not msg.isRead then speak msg.message, speed: 140, amplitude: 200, pitch: 30, wordgap: 10
+	allMessages.observe added: (msg) -> if not msg.isRead then speak msg.message, speed: 140, amplitude: 200, pitch: 30, wordgap: 10

 	getMessage = -> messages.findOne {}

@@ -23,7 +24,7 @@
 		if isRead() and messages.add textbox.value then textbox.value = ''

 	_.extend Template.main,
-		messages: -> messages.find {}
+		messages: -> allMessages
 		inputClassName: -> if isRead() then 'read' else 'unread'
 		events:
 			'click button': addMessage


Son is removed from about page

Son is removed from about page

--- client/app/Applications/Home.kdapplication/ContentDisplays/AboutView.coffee
+++ client/app/Applications/Home.kdapplication/ContentDisplays/AboutView.coffee
@@ -97,11 +97,11 @@
         nickname  : 'gokmen'
         job       : 'Software Engineer'
         image     : '../images/people/gokmen.jpg'
-      ,
-        name      : 'Son Tran-Nguyen'
-        nickname  : 'sntran'
-        job       : 'Software Engineer'
-        image     : '../images/people/son.jpg'
+      # ,
+      #   name      : 'Son Tran-Nguyen'
+      #   nickname  : 'sntran'
+      #   job       : 'Software Engineer'
+      #   image     : '../images/people/son.jpg'
       ,
         name      : 'Arvid Kahl'
         nickname  : 'arvidkahl'


Track activity feed

Track activity feed

--- client/landing/app/Applications/Activity.kdapplication/AppController.coffee
+++ client/landing/app/Applications/Activity.kdapplication/AppController.coffee
@@ -33,6 +33,7 @@

   constructor:(options={})->
     options.view    = new ActivityAppView
+      testPath      : "activity-feed"
     options.appInfo =
       name          : 'Activity'


neo4j: fix duplicate activities (wip)

neo4j: fix duplicate activities (wip)

--- client/landing/app/Applications/Activity.kdapplication/AppController.coffee
+++ client/landing/app/Applications/Activity.kdapplication/AppController.coffee
@@ -148,8 +148,8 @@

   # Store first & last cache activity timestamp.
   extractCacheTimeStamps: (cache)->
-    #@lastTo   = cache.to
-    #@lastFrom = cache.from
+    @lastTo   = cache.to
+    @lastFrom = cache.from

   # Refreshes activity feed, used when user has been disconnected
   # for so long, backend connection is long gone.


Check if property exists.

Check if property exists.

--- app/assets/javascripts/_site.js.coffee
+++ app/assets/javascripts/_site.js.coffee
@@ -20,7 +20,7 @@
   colorHash: new ColorHash()

   faviconColors: (target) ->
-    if feedbin.data.favicon_colors
+    if feedbin && feedbin.data && feedbin.data.favicon_colors
       $(".favicon-default", target).each ->
         host = $(@).data("host")
         color = feedbin.colorHash.hex(host)


removes `console.log`s

removes `console.log`s

--- app/assets/javascripts/issue.js.coffee
+++ app/assets/javascripts/issue.js.coffee
@@ -27,7 +27,6 @@
         url: url,
         error: (jqXHR, textStatus, errorThrown) ->
           issueStatus = if isClose then 'close' else 'open'
-          console.log("Cannot #{issueStatus} this issue, at this time.")
         success: (data, textStatus, jqXHR) ->
           if data.saved
             $this.addClass('hidden')
@@ -40,7 +39,6 @@
               $('div.issue-box-closed').addClass('hidden')
               $('div.issue-box-open').removeClass('hidden')
           else
-            console.log("Did not work")
           $this.prop('disabled', false)

   disableTaskList: ->


namespace只能一级

namespace只能一级

--- lib/build/build-state-view.coffee
+++ lib/build/build-state-view.coffee
@@ -57,7 +57,8 @@
     console.log "try to connect."

     # puzzleClient.server: http://bsl.foreveross.com/puzzle/socketio 服务器默认的path为/
-    @socket = io "#{puzzleClient.server}/socketio",
+    hostName = puzzleClient.server.substr 0, puzzleClient.server.lastIndexOf('/')
+    @socket = io "#{hostName}/socketio",
       reconnection: true
       reconnectionDelay: 50
       reconnectionDelayMax: 12000


fix(admin) Remove project request

fix(admin) Remove project request

--- app/coffee/modules/base/contrib.coffee
+++ app/coffee/modules/base/contrib.coffee
@@ -65,8 +65,5 @@

     constructor: (@rootScope, @scope, @params) ->
         @scope.currentPlugin = _.head(_.filter(@rootScope.userSettingsPlugins, {"slug": @params.plugin}))
-        @.loadInitialData()

-    loadInitialData: ->
-        return @.loadProject()
 module.controller("ContribUserSettingsController", ContribUserSettingsController)


crud tests

crud tests

mocha test\crud.coffee -R spec --compilers coffee:coffee-script

--- test/crud.coffee
+++ test/crud.coffee
@@ -7,23 +7,75 @@

 describe "restify-nedb crud tests", () ->

-  describe "GET /session/v1", () ->
-    it "should respond with status 200", (done) ->
-
-      request uri + "/session/v1", (err, resp, body) ->
+  describe "POST insert new test seed data", () ->
+    it "should return a new item that matches our seed data", (done) ->
+
+      opts = {uri: "#{uri}/session/v1", form: {test: "test posting new object"}, method: "post"}
+
+      request opts, (err, resp, body) ->
+        expect(err).to.be(null)
         expect(resp.statusCode).to.eql(200)
         expect(body).not.to.be(undefined)
-        expect(err).to.be(null)
-        _id = body[0]._id
+        json = JSON.parse body
+        _id = json._id
+        expect(_id).not.to.be(undefined)
+        expect(json.stale).not.to.be(undefined)
         done()

-  describe "GET /session/v1/#{_id}", () ->
+  describe "GET by _id", () ->
     it "should return a single result matching it's _id", (done) ->

       request "#{uri}/session/v1/#{_id}", (err, resp, body) ->
         expect(resp.statusCode).to.eql(200)
         expect(body).not.to.be(undefined)
         expect(err).to.be(null)
-        expect(body._id).to.be(_id)
-        expect(body.stale)
+        json = JSON.parse body
+        _id = json[0]._id
+        expect(_id).not.to.be(undefined)
+        expect(json[0].stale).not.to.be(undefined)
         done()
+
+  describe "PUT", () ->
+    it "should update our test object without appending", (done) ->
+
+      opts = {uri: "#{uri}/session/v1/#{_id}", form: {test: "test updating an object without appending"}, method: "put"}
+
+      request opts, (err, resp, body) ->
+        expect(err).to.be(null)
+        expect(resp.statusCode).to.eql(200)
+        expect(body).not.to.be(undefined)
+        done()
+
+  describe "PUT ?=append=true", () ->
+    it "should update our test object with append", (done) ->
+
+      query = "?append=true"
+      opts = {uri: "#{uri}/session/v1/#{_id}#{query}", form: {append: "test updating an object with append"}, method: "put"}
+
+      request opts, (err, resp, body) ->
+        expect(err).to.be(null)
+        expect(resp.statusCode).to.eql(200)
+        expect(body).not.to.be(undefined)
+        done()
+
+  describe "GET", () ->
+    it "should respond with status 200", (done) ->
+
+      request uri + "/session/v1", (err, resp, body) ->
+        expect(err).to.be(null)
+        expect(resp.statusCode).to.eql(200)
+        expect(body).not.to.be(undefined)
+        done()
+
+  describe "DELETE our seed data", () ->
+    it "should delete our seed data", (done) ->
+
+      opts = {uri: "#{uri}/session/v1/#{_id}", method: "delete"}
+
+      request opts, (err, resp, body) ->
+        expect(err).to.be(null)
+        expect(resp.statusCode).to.eql(200)
+        expect(body).not.to.be(undefined)
+        json = JSON.parse body
+        expect(json.num_deleted).to.be(1)
+        done()


Use Turbolinks 5 events for clearing event handlers.

--- lib/generators/rails_script/install/templates/base.js.coffee
+++ lib/generators/rails_script/install/templates/base.js.coffee
@@ -37,7 +37,7 @@
   loads, i.e. "$(window).on 'scroll.app', myHandler"
   ###
   setClearEventHandlers: ->
-    jQuery(document).on 'page:before-change', ->
+    jQuery(document).on 'turbolinks:before-visit', ->
       for element in [window, document]
         for event, handlers of (jQuery._data(element, 'events') || {})
           for handler in handlers


Account Settings: Credentials: Don't show koding and custom providers in filter list.

--- client/account/lib/accountcredentiallistwrapper.coffee
+++ client/account/lib/accountcredentiallistwrapper.coffee
@@ -44,6 +44,9 @@
     providers = @getValidProviders()

     for provider in providers
+
+      continue  if provider.key is 'custom' or provider.key is 'koding'
+
       selectOptions.push
         title : provider.title
         value : provider.key


StacksCustomViews: stepFetchTemplate: parse and show info about stack template

--- client/admin/lib/views/stacks/stackscustomviews.coffee
+++ client/admin/lib/views/stacks/stackscustomviews.coffee
@@ -513,36 +513,67 @@
       { repo_provider, selected_repo }   = data

       container = @views.container 'step-fetch-template'
+      container.setClass 'has-markdown'

       views     = @addTo container,
         stepsHeaderView   :
           steps           : STEPS.REPO_FLOW
           selected        : 3
         mainLoader        : 'Fetching template...'
-        outputView        :
-          cssClass        : 'hidden'
+        container         : 'output-container'
         navCancelButton   :
-          title           : '< Select another repo'
+          title           : '< Select another template'
           callback        : ->
             cancelCallback data

-      { outputView, mainLoader } = views
-
-      fetchRepoFile selected_repo, (err, template) ->
-
-        mainLoader.hide()
-        outputView.show()
+      {mainLoader} = views
+
+      fetchRepoFile selected_repo, (err, template) =>
+
+        mainLoader.setTitle 'Parsing template...'

         console.log err, template

         if err
           content = err?.message
+          message  = "We've failed to fetch the template, from given branch/tag
+                      with given file name. Please make sure you are providing
+                      a valid template path like described
+                      here."
+
         else if template?.content?
+
           content = atob template.content
+
+          try
+            template = JSON.parse content
+          catch e
+            message  = "We've failed to parse the template, please make sure
+                        you are providing a valid template like described
+                        here."
+
+          if template
+            list      = ''
+            providers = Object.keys template.provider
+            list     += "
#{pr}\n" for pr in providers
+
+            message   = "Based on the template you will need to enter
+                         credetentials for the following providers;

+                         #{list}"
+
         else
           content = 'Something went wrong, please try again.'

+
+        {outputView} = @addTo views.container,
+
+          container_top :
+            text        : message
+          outputView    :
+            cssClass    : 'template-output'
+
         outputView.setContent content
+        mainLoader.hide()

       return container


NavigationMachineItem: Minor refactor for modal positions.

--- client/app/lib/navigation/navigationmachineitem.coffee
+++ client/app/lib/navigation/navigationmachineitem.coffee
@@ -105,17 +105,15 @@

     kd.utils.stopDOMEvent event

-    @openMachineSettingsPopup()
+    new MachineSettingsPopup { position: @getPopupPosition() }, @machine

-  openMachineSettingsPopup: ->
+  getPopupPosition: ->

     bounds   = @getBounds()
     position =
       top    : Math.max   bounds.y - 38, 0
       left   : bounds.x + bounds.w + 16
-
-    new MachineSettingsPopup { position }, @machine

   handleMachineEvent: (event) ->


payment: remove commented-out code

payment: remove commented-out code

--- client/app/Applications/Account.kdapplication/views/paymentmethods.coffee
+++ client/app/Applications/Account.kdapplication/views/paymentmethods.coffee
@@ -67,8 +67,6 @@
       else
         modal.setState 'createNew'

-        # modal.setPaymentInfo initialPaymentInfo.billing
-
     paymentController.observePaymentSave modal, (err, updatedPaymentInfo) =>
       if err
         new KDNotificationView title: err.message


groups: alternative cascade for KDModalViewWithForms

--- client/landing/app/Applications/Groups.kdapplication/AppController.coffee
+++ client/landing/app/Applications/Groups.kdapplication/AppController.coffee
@@ -314,6 +314,9 @@

         cascadeFormElements = (set,roles,module,permission)->
           [current,remainder...] = roles
+          # alternative implementation:
+          # current = roles.slice().shift() # then pass roles instead of remainder
+
           data = {}
           data[current]=
               itemClass     : KDCheckBox


Test condition with exec and ssh

Test condition with exec and ssh

--- test/exec.coffee
+++ test/exec.coffee
@@ -3,6 +3,7 @@
 should = require 'should'
 mecano = if process.env.MECANO_COV then require '../lib-cov/mecano' else require '../lib/mecano'
 test = require './test'
+connect = require 'superexec/lib/connect'

 describe 'exec', ->

@@ -94,4 +95,14 @@
         should.not.exist stdout
         next()

+  it 'should not run ssh command if file exists', (next) ->
+    connect host: 'localhost', (err, ssh) ->
+      mecano.exec
+        ssh: ssh
+        cmd: "ls -l #{__dirname}"
+        not_if_exists: __dirname
+      , (err, executed, stdout, stderr) ->
+        return next err if err
+        executed.should.eql 0
+        next()


Added way to load global interface plugins last

--- src/core/pluginsEngine/plugins.coffee
+++ src/core/pluginsEngine/plugins.coffee
@@ -1,6 +1,7 @@
 async = require 'async'
 jf = require 'jsonfile'
 fs = require 'fs'
+colors = require 'colors'

 module.exports = (env) ->
 	env.debug 'Initializing plugins engine'
@@ -34,24 +35,45 @@
 		env.hooks[name] ?= []
 		env.hooks[name].push fn

+
+	global_interface = undefined
 	pluginsEngine.load = (plugin_name) ->
-		env.debug "Loading '" + plugin_name + "'."
+
 		try
 			plugin_data = require(env.pluginsEngine.cwd + '/plugins/' + plugin_name + '/plugin.json')
-		catch
-			env.debug 'Absent plugin.json for plugin \'' + plugin_name + '\'.'
-			plugin_data = {}
+		catch e
+			env.debug 'Error loading plugin.json (' + plugin_name + ')'
+			console.log e.message.yellow
+			plugin_data = {
+				name: plugin_name
+			}
+
 		if plugin_data.main?
-			entry_point = '/' + plugin_data.main
+			prefix = '/' if plugin_data.main[0] != '/'
+			plugin_data.main = prefix + plugin_data.main
 		else
-			entry_point = '/index'
+			plugin_data.main = '/index'
+
+		if not plugin_data.name? or plugin_data.name != plugin_name
+			plugin_data.name = plugin_name
+
+
+		if plugin_data.type != 'global-interface'
+			loadPlugin(plugin_data)
+		else
+			global_interface = plugin_data
+
+
+	loadPlugin = (plugin_data) ->
+		env.debug "Loading " + plugin_data.name.blue
 		try
-			plugin = require(env.pluginsEngine.cwd + '/plugins/' + plugin_name + entry_point)(env)
-			env.config.plugins.push plugin_name
-			pluginsEngine.plugin[plugin_name] = plugin
+			plugin = require(env.pluginsEngine.cwd + '/plugins/' + plugin_data.name + plugin_data.main)(env)
+			pluginsEngine.plugin[plugin_data.name] = plugin
 		catch e
-			env.debug "Error requiring plugin \'" + plugin_name + "\' entry point."
-		return
+			env.debug "Error while loading plugin " + plugin_data.name
+			env.debug e.message.yellow + ' at line ' + e.lineNumber.red
+
+

 	pluginsEngine.init = (cwd, callback) ->
 		env.pluginsEngine.cwd = cwd
@@ -65,6 +87,8 @@
 				stat = fs.statSync cwd + '/plugins/' + pluginname
 				if stat.isDirectory()
 					pluginsEngine.load pluginname
+			if global_interface?
+				loadPlugin(global_interface)
 			return callback false

 	pluginsEngine.list = (callback) ->


accepting coffee manifests

accepting coffee manifests

--- client/landing/app/MainApp/kodingappscontroller.coffee
+++ client/landing/app/MainApp/kodingappscontroller.coffee
@@ -83,34 +83,39 @@

     path = "/Users/#{KD.whoami().profile.nickname}/Applications"

-    @kiteController.run "ls #{escapeFilePath path} -lpva", (err, response)=>
-      if err
-        warn err
-        callback err
-      else
-        files = FSHelper.parseLsOutput [path], response
-        apps  = []
-        stack = []
-
-        files.forEach (file)->
-          if /\.kdapp$/.test file.name
-            apps.push file
-
-        apps.forEach (app)->
-          manifest = if app.type is "folder" then FSHelper.createFileFromPath "#{app.path}/.manifest" else app
-          stack.push (cb)->
-            manifest.fetchContents cb
-
-        manifests = @constructor.manifests
-        async.parallel stack, (err, results)->
-          if err
-            warn err
-            callback? err
-          else
-            results.forEach (app)->
-              app = JSON.parse app
-              manifests["#{app.name}"] = app
-            callback? err, manifests
+    require ["coffee-script"], (coffee)=>
+      @kiteController.run "ls #{escapeFilePath path} -lpva", (err, response)=>
+        if err
+          warn err
+          callback err
+        else
+          files = FSHelper.parseLsOutput [path], response
+          apps  = []
+          stack = []
+
+          files.forEach (file)->
+            if /\.kdapp$/.test file.name
+              apps.push file
+
+          apps.forEach (app)->
+            manifestFile = if app.type is "folder" then FSHelper.createFileFromPath "#{app.path}/.manifest" else app
+            stack.push (cb)->
+              manifestFile.fetchContents cb
+
+          manifests = @constructor.manifests
+          async.parallel stack, (err, results)->
+            if err
+              warn err
+              callback? err
+            else
+              results.forEach (rawManifest)->
+                if rawManifest.substr(0,1) is '{'
+                  manifest = JSON.parse rawManifest
+                else
+                  manifest = coffee.compile rawManifest, { bare : yes }
+                  log manifest, ">>>>"
+                manifests["#{manifest.name}"] = manifest
+              callback? err, manifests

   fetchAppsFromDb:(callback)->


Make Like tooltip sentences more polite

Make Like tooltip sentences more polite

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

           @likeCount.updateTooltip {title: tooltip }


Implemented cubic interpolation

Implemented cubic interpolation

--- Smooth.coffee
+++ Smooth.coffee
@@ -39,11 +39,11 @@
 	i = period - i if i > n - 1 #flip when out of bounds
 	i

-getFraction = (x) -> x - Math.floor x

 class AbstractInterpolator

-	constructor: (@array, config) ->
+	constructor: (array, config) ->
+		@array = array.slice 0 #copy the array
 		@length = @array.length #cache length

 		#Set the clipping helper method
@@ -90,11 +90,28 @@
 #Linear interpolator (first order Bezier)
 class LinearInterpolator extends AbstractInterpolator
 	interpolate: (t) ->
-		i = Math.floor t
-		a = @getClippedInput i
-		b = @getClippedInput i+1
-		t = getFraction t
+		k = Math.floor t
+		a = @getClippedInput k
+		b = @getClippedInput k+1
+		#Translate t to interpolate between k and k+1
+		t -= k
 		return (1-t)*a + (t)*b
+
+
+class CubicInterpolator extends AbstractInterpolator
+	# Finite difference (because of uniformity, equivalent to Cardinal spline with tension 0.5)
+	getTangent: (k) -> 0.5*(@getClippedInput(k + 1) - @getClippedInput(k - 1))
+
+	interpolate: (t) ->
+		k = Math.floor t
+		m = [(@getTangent k), (@getTangent k+1)] #get tangents
+		p = [(@getClippedInput k), (@getClippedInput k+1)] #get points
+		#Translate t to interpolate between k and k+1
+		t -= k
+		t2 = t*t #t^2
+		t3 = t*t2 #t^3
+		#Apply cubic hermite spline formula
+		return (2*t3 - 3*t2 + 1)*p[0] + (t3 - 2*t2 + t)*m[0] + (-2*t3 + 3*t2)*p[1] + (t3 - t2)*m[1]


removed random javascript typo

removed random javascript typo


instantiate helpers in init_roots obj; compile all dependencies first

--- test/test.coffee
+++ test/test.coffee
@@ -4,10 +4,10 @@
 fs         = require "fs"
 glob       = require "glob"
 W          = require "when"
-RootsUtil  = require "roots-util"
 nodefn     = require "when/node"
 run        = require("child_process").exec
 _fixtures  = path.join(__dirname, 'fixtures')
+RootsUtil  = require "roots-util"
 _roots     = path.join(_fixtures, 'roots')
 _projects  = {
   url: path.join(_roots, "url"),
@@ -20,14 +20,15 @@

 init_roots = (base_path, compile_callback) ->
   roots = new Roots(base_path)
-  helpers = new RootsUtil.Helpers
-  helpers.project.install_dependencies(base_path, ->
-    roots.compile()
-      .on('done', compile_callback))
-
-  { roots: roots, helpers: helpers, public: path.join(base_path, "public") }
+  roots.compile()
+    .on('done', compile_callback)
+  { roots: roots, helpers: new RootsUtil.Helpers(base: base_path) }

 describe 'records', ->
+
+  before (done) ->
+    helpers = new RootsUtil.Helpers(base: _fixtures)
+    helpers.project.install_dependencies("*", done)

   describe 'url', ->

@@ -47,7 +48,7 @@
     describe 'compiled template', ->

       it "should contain 'books'", ->
-        @_.helpers.file.contains(path.join(@_.public, "index.html"), "books").should.be.ok
+        @_.helpers.file.contains(path.join("public", "index.html"), "books").should.be.ok

   describe 'file', ->

@@ -67,7 +68,7 @@
     describe 'compiled template', ->

       it "should have {foo: 'bar'} json", ->
-        @_.helpers.file.contains(path.join(@_.public, "index.html"), JSON.stringify({foo: "bar"})).should.be.ok
+        @_.helpers.file.contains(path.join("public", "index.html"), JSON.stringify({foo: "bar"})).should.be.ok

   describe 'data', ->

@@ -87,7 +88,7 @@
     describe 'compiled template', ->

       it "should have {foo: 'bar'} json", ->
-        @_.helpers.file.contains(path.join(@_.public, "index.html"), JSON.stringify({foo: "bar"})).should.be.ok
+        @_.helpers.file.contains(path.join("public", "index.html"), JSON.stringify({foo: "bar"})).should.be.ok

   describe 'invalid key', ->


Add Steven and Carlos to About Us

Add Steven and Carlos to About Us

--- views/outer/about/_team.coffee
+++ views/outer/about/_team.coffee
@@ -16,3 +16,19 @@
     email: "allen@circleci.com"
     bio: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam at eros non dui sollicitudin mattis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aenean elementum mi tincidunt orci rhoncus tempus. Cras tincidunt feugiat purus a venenatis.  Morbi sollicitudin turpis sapien, sed consequat justo. Sed semper sagittis ornare. Etiam et est tortor, at tristique sem. Maecenas a quam magna."
     visible: true
+
+  steven:
+    name: "Steven! Ragnarök"
+    role: "Developer"
+    github: "nuclearsandwich"
+    email: "steven@circleci.com"
+    bio: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam at eros non dui sollicitudin mattis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aenean elementum mi tincidunt orci rhoncus tempus. Cras tincidunt feugiat purus a venenatis.  Morbi sollicitudin turpis sapien, sed consequat justo. Sed semper sagittis ornare. Etiam et est tortor, at tristique sem. Maecenas a quam magna."
+    visible: true
+
+  carlos:
+    name: "Carlos Antonio"
+    role: "Designer"
+    github: "nuclearsandwich"
+    email: "steven@circleci.com"
+    bio: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam at eros non dui sollicitudin mattis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aenean elementum mi tincidunt orci rhoncus tempus. Cras tincidunt feugiat purus a venenatis.  Morbi sollicitudin turpis sapien, sed consequat justo. Sed semper sagittis ornare. Etiam et est tortor, at tristique sem. Maecenas a quam magna."
+    visible: true


Formatters to static_select2

Formatters to static_select2

- added formatters to static_select2

--- vendor/assets/javascripts/auto_select2/static_select2.js.coffee
+++ vendor/assets/javascripts/auto_select2/static_select2.js.coffee
@@ -13,9 +13,17 @@
         s2FullOptions = $.extend({}, s2DefaultOptions)
       else
         s2FullOptions = $.extend({}, s2DefaultOptions, s2UserOptions)
+        customFormatSelection = s2UserOptions.formatSelection
+        customFormatResult = s2UserOptions.formatResult
+
+      if customFormatSelection isnt `undefined` && (window[customFormatSelection] isnt `undefined`)
+        formatSelectionFunc = window[customFormatSelection]
+      if (customFormatResult isnt `undefined`) && (window[customFormatResult] isnt `undefined`)
+        formatResultFunc = window[customFormatResult]
+
+      s2FullOptions = $.extend({}, s2FullOptions, {formatSelection: formatSelectionFunc, formatResult: formatResultFunc})

       $input.select2(s2FullOptions)
-
       return
     return
   initAutoStaticSelect2()


Removed debug output

Removed debug output

--- vendor/assets/javascripts/editable/rails/data_classes.js.coffee
+++ vendor/assets/javascripts/editable/rails/data_classes.js.coffee
@@ -14,7 +14,6 @@

 jQuery ($) ->
   $("[data-classes]").on "save", (e, data) ->
-    console?.log('data', data, 'event', e)
     value = data.newValue
     element = $(this)
     css = element.data('classes') || {}


Fix incorrect translation of unbracketed then/else

--- src/documents/lib/js2coffee.js.coffee
+++ src/documents/lib/js2coffee.js.coffee
@@ -837,7 +837,7 @@

   'if': (n) ->
     # *Account for `if(x) {} else { something }` which should be `something unless x`.*
-    if n.thenPart.children.length == 0 and n.elsePart?.children.length > 0
+    if n.thenPart.isA('block') and n.thenPart.children.length == 0 and (!n.elsePartisA('block') or n.elsePart?.children.length > 0)
       n.positive = false
       n.thenPart = n.elsePart
       delete n.elsePart


Custom OAuth fixes for Reddit (#2921)

Custom OAuth fixes for Reddit (#2921)

* Fix typo with @tokenSentVia in Custom OAuth

* Make custom OAuth work with Reddit

--- packages/rocketchat-custom-oauth/custom_oauth_server.coffee
+++ packages/rocketchat-custom-oauth/custom_oauth_server.coffee
@@ -55,6 +55,7 @@
 		response = undefined
 		try
 			response = HTTP.post @tokenPath,
+				auth: config.clientId + ':' + OAuth.openSecret(config.secret)
 				headers:
 					Accept: 'application/json'
 					'User-Agent': @userAgent
@@ -107,6 +108,10 @@

 			identity = self.getIdentity accessToken

+			# Fix for Reddit
+			if identity?.result
+				identity = identity.result
+
 			# Fix WordPress-like identities having 'ID' instead of 'id'
 			if identity?.ID and not identity.id
 				identity.id = identity.ID


Incorrect update of templates list (fix vatesfr/xo-web#55).

--- src/MappedCollection.coffee
+++ src/MappedCollection.coffee
@@ -272,7 +272,11 @@
           prev = @_byKey[key]

           # Checks if there is a conflict in rules.
-          @ruleConflict item.rule, prev unless item.rule is prev.rule
+          unless item.rule is prev.rule
+            @ruleConflict item.rule, prev
+            item.prevRule = prev.rule
+          else
+            delete item.prevRule

           # Gets its previous data/value.
           item.data = prev.data
@@ -309,13 +313,18 @@

   # Emits item related event.
   _emitEvent: (event, items) ->
+    getRule = if event is 'exit'
+      (item) -> item.prevRule or item.rule
+    else
+      (item) -> item.rule
+
     byRule = Object.create null

     # One per item.
     $each items, (item) =>
       @emit "key=#{item.key}", event, item

-      (byRule[item.rule] ?= []).push item
+      (byRule[getRule item] ?= []).push item

     # One per rule.
     @emit "rule=#{rule}", event, byRule[rule] for rule of byRule


Fix bug with view events

Fix bug with view events

--- app/models/experimental_subject.coffee
+++ app/models/experimental_subject.coffee
@@ -182,9 +182,9 @@
         @advance fetcher, callback
         if fakeFetcher?
           fakeFetcher.resolve()
-        AnalyticsLogger.logEvent 'view'
+        AnalyticsLogger.logEvent 'view',null,User.current?.zooniverse_id,@current.zooniverse_id
       else
-        AnalyticsLogger.logEvent 'view'
+        AnalyticsLogger.logEvent 'view',null,User.current?.zooniverse_id,@current.zooniverse_id
         # wrong experiment running - revert to parent
         super.next callback
     else


:lipstick: filePath instead of path

:lipstick: filePath instead of path

--- spec/image-editor-view-spec.coffee
+++ spec/image-editor-view-spec.coffee
@@ -2,11 +2,11 @@
 ImageEditor = require '../lib/image-editor'

 describe "ImageEditorView", ->
-  [editor, view, path] = []
+  [editor, view, filePath] = []

   beforeEach ->
-    path = atom.project.resolve('binary-file.png')
-    editor = new ImageEditor({path})
+    filePath = atom.project.resolve('binary-file.png')
+    editor = new ImageEditor({path: filePath})
     view = new ImageEditorView(editor)
     view.attachToDom()
     view.height(100)
@@ -14,7 +14,7 @@
     waitsFor -> view.loaded

   it "displays the image for a path", ->
-    expect(view.image.attr('src')).toBe path
+    expect(view.image.attr('src')).toBe filePath

   it "centers the image in the editor", ->
     expect(view.image.width()).toBe 10


:white_check_mark: Add tests for project find whole word option

--- spec/project-find-view-spec.coffee
+++ spec/project-find-view-spec.coffee
@@ -108,6 +108,7 @@
     it "honors config settings for find options", ->
       atom.config.set('find-and-replace.useRegex', true)
       atom.config.set('find-and-replace.caseSensitive', true)
+      atom.config.set('find-and-replace.wholeWord', true)

       atom.commands.dispatch(workspaceElement, 'project-find:show')

@@ -117,6 +118,7 @@
       runs ->
         expect(projectFindView.caseOptionButton).toHaveClass 'selected'
         expect(projectFindView.regexOptionButton).toHaveClass 'selected'
+        expect(projectFindView.wholeWordOptionButton).toHaveClass 'selected'

   describe "when project-find:toggle is triggered", ->
     it "toggles the visibility of the ProjectFindView", ->
@@ -360,7 +362,7 @@
           expect(resultsPaneView2.querySelector('.preview-count').innerHTML).toEqual resultsPaneView1.querySelector('.preview-count').innerHTML

     describe "serialization", ->
-      it "serializes if the case and regex options", ->
+      it "serializes if the case, regex and whole word options", ->
         atom.commands.dispatch editorView, 'project-find:show'
         expect(projectFindView.caseOptionButton).not.toHaveClass('selected')
         projectFindView.caseOptionButton.click()
@@ -370,6 +372,10 @@
         projectFindView.regexOptionButton.click()
         expect(projectFindView.regexOptionButton).toHaveClass('selected')

+        expect(projectFindView.wholeWordOptionButton).not.toHaveClass('selected')
+        projectFindView.wholeWordOptionButton.click()
+        expect(projectFindView.wholeWordOptionButton).toHaveClass('selected')
+
         atom.packages.deactivatePackage("find-and-replace")

         activationPromise = atom.packages.activatePackage("find-and-replace").then ({mainModule}) ->
@@ -384,6 +390,7 @@
         runs ->
           expect(projectFindView.caseOptionButton).toHaveClass('selected')
           expect(projectFindView.regexOptionButton).toHaveClass('selected')
+          expect(projectFindView.wholeWordOptionButton).toHaveClass('selected')

     describe "description label", ->
       beforeEach ->
@@ -510,6 +517,39 @@
         runs ->
           expect(projectFindView.caseOptionButton).toHaveClass('selected')
           expect(atom.workspace.scan.mostRecentCall.args[0]).toEqual /ITEMS/g
+
+    describe "whole word", ->
+      beforeEach ->
+        atom.commands.dispatch editorView, 'project-find:show'
+        spyOn(atom.workspace, 'scan').andCallFake -> Q()
+        projectFindView.findEditor.setText('wholeword')
+        atom.commands.dispatch(projectFindView[0], 'core:confirm')
+        waitsForPromise -> searchPromise
+
+      it "does not run whole word search by default", ->
+        expect(atom.workspace.scan.argsForCall[0][0]).toEqual /wholeword/gi
+
+      it "toggles whole word option via an event and finds files matching the pattern", ->
+        expect(projectFindView.wholeWordOptionButton).not.toHaveClass('selected')
+        atom.commands.dispatch(projectFindView[0], 'project-find:toggle-whole-word-option')
+
+        waitsForPromise ->
+          searchPromise
+
+        runs ->
+          expect(projectFindView.wholeWordOptionButton).toHaveClass('selected')
+          expect(atom.workspace.scan.mostRecentCall.args[0]).toEqual /\bwholeword\b/gi
+
+      it "toggles whole word option via a button and finds files matching the pattern", ->
+        expect(projectFindView.wholeWordOptionButton).not.toHaveClass('selected')
+        projectFindView.wholeWordOptionButton.click()
+
+        waitsForPromise ->
+          searchPromise
+
+        runs ->
+          expect(projectFindView.wholeWordOptionButton).toHaveClass('selected')
+          expect(atom.workspace.scan.mostRecentCall.args[0]).toEqual /\bwholeword\b/gi

     describe "when project-find:confirm is triggered", ->
       it "displays the results and no errors", ->