Add new Grunt task 'nofinish'.

Add new Grunt task 'nofinish'.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -84,3 +84,5 @@
   # Default task(s).
   grunt.registerTask 'default', ['coffee', 'cssmin', 'uglify', 'inline', 'finish']

+  grunt.registerTask 'nofinish', ['coffee', 'cssmin', 'uglify', 'inline']
+


Remove unused require

Remove unused require

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -1,4 +1,3 @@
-{spawn} = require 'child_process'
 fs = require 'fs'
 path = require 'path'


organizing connect middlewares

organizing connect middlewares

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -4,6 +4,14 @@
 		'/sign': (req, res, next) ->
 			throw new Error "not implemented"
 			return
+
+	helpers =
+		middlewares:
+			known_route_service: (req, res, next) ->
+				service = routes[req.url]
+				return next() unless service?
+				service req, res, next
+				return

 	grunt.initConfig
 		connect:
@@ -13,11 +21,9 @@
 					hostname: '*'
 					base: '.'
 					middleware: (connect, options, middlewares) ->
-						middlewares.unshift (req, res, next) ->
-							service = routes[req.url]
-							return next() unless service?
-							service req, res, next
-							return
+						middlewares[-1..-2] = [
+							helpers.middlewares.known_route_service
+						]
 						middlewares
 		jasmine:
 			options:


UMD task should be after Coffee task

UMD task should be after Coffee task

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -77,8 +77,8 @@
   grunt.registerTask('test', ['karma'])

   grunt.registerTask('default', [
-    'umd',
     'coffee',
     'less',
+    'umd',
     'uglify'
   ])


Simplify (remove array syntax)

Simplify (remove array syntax)

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -73,8 +73,8 @@
           banner: banner_template
         expand: true
         cwd: 'build/css/'
-        src: ['**/*.css']
         dest: 'build/css/'
+        src: '*.css'
         ext: '.min.css'
     uglify:
       options: {


Also copy license file to dist

Also copy license file to dist

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -19,13 +19,13 @@
       main:
         files: [
           expand: true,
-          src: ["public/**", "views/**", "node/**", "START.cmd", "INSTALL.cmd", "package.json"],
+          src: ["public/**", "views/**", "node/**", "START.cmd", "INSTALL.cmd", "package.json", "LICENSE.txt"],
           dest: "dist/"
         ]
       fast:
         files: [
           expand: true,
-          src: ["public/**", "views/**", "START.cmd", "INSTALL.cmd", "package.json"],
+          src: ["public/**", "views/**", "START.cmd", "INSTALL.cmd", "package.json", "LICENSE.txt"],
           dest: "dist/"
         ]


Switch to bare CoffeeScript.

Switch to bare CoffeeScript.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -8,6 +8,8 @@

     @initConfig
         coffee:
+            options:
+                bare: true
             all:
                 expand: true,
                 cwd: 'src',


Add Bower deps to the PhoneGap build

Add Bower deps to the PhoneGap build

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -65,6 +65,10 @@
           dest: '/'
         ,
           src: ['browser/noflo-ui.js']
+          expand: true
+          dest: '/'
+        ,
+          src: ['bower_components/*']
           expand: true
           dest: '/'
         ,


changed naming convention on templates

changed naming convention on templates

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -38,7 +38,7 @@
           compileDebug: false
           client: true
           namespace: 'app.templates'
-          processName: (file)-> file.replace(/views\/client\/([\w\/]+).jade/gi, '$1').replace('/', '_')
+          processName: (file)-> file.replace(/views\/client\/([\w\/]+).jade/gi, '$1')
         src: 'views/client/**/*.jade'
         dest: 'public/assets/views.js'


Run npm build from rebuild task

Run npm build from rebuild task

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -32,7 +32,7 @@

     shell:
       rebuild:
-        command: 'node-gyp rebuild'
+        command: 'npm build .'
         options:
           stdout: true
           stderr: true


remove remnants of grunt-open grunt server was broken

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -594,7 +594,6 @@
 	# grunt server
 	grunt.registerTask 'server', [
 		'connect'
-		'open'
 		'watch:none'
 	]


chore(grunt): minor tweak

chore(grunt): minor tweak

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -27,6 +27,7 @@
     app: "<%= config.cfg.source %>"
     dist: "<%= config.cfg.destination %>"
     base: "<%= config.cfg.base %>"
+    assets: "<%= config.cfg.assets %>"
     banner: "<!-- <%= config.pkg.name %> v<%= config.pkg.version %> | © <%= config.pkg.author %> | <%= config.pkg.license %> -->\n"

   # TODO: Wrap it into a separate Grunt plugin?
@@ -56,7 +57,7 @@
       branch: grunt.option("branch") or "release"
       core: "<%= amsf.base %>/core"
       user:
-        assets: "<%= config.app %>/assets"
+        assets: "<%= config.app %><%= config.assets %>"
       theme:
         branch: "master"
         assets: "<%= amsf.user.assets %>/themes/<%= amsf.theme.current %>"
@@ -262,7 +263,7 @@
         inlineImg: false
         inlineSvg: true
         inlineSvgBase64: false
-        assetsUrlPrefix: "<%= config.base %>/assets/"
+        assetsUrlPrefix: "<%= config.base %><%= config.assets %>"
         deleteOriginals: true

       dist:


build and expose the new alternative in favor of easyXDM

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -28,7 +28,6 @@
           'build/js/jail_iframe/libs/jquery-1.7.2.js'
           'build/js/jail_iframe/core.js'
           'build/js/jail_iframe/wrap/first.js'
-          'build/js/jail_iframe/libs/easyXDM.js'
           'build/js/jail_iframe/plugins/*.js'
           'build/js/jail_iframe/classes/*.js'
           'build/js/jail_iframe/views/*.js'
@@ -96,6 +95,7 @@
           'build/factlink_loader_publishers.min.js':  ['build/factlink_loader_publishers.js']
           'build/factlink_loader_bookmarklet.min.js': ['build/factlink_loader_bookmarklet.js']
           'build/easyXDM/easyXDM.min.js':                    ['build/js/jail_iframe/libs/easyXDM.js']
+          'build/postFactlinkObject.min.js': ['build/js/jail_iframe/util/postFactlinkObject.js']
     shell:
       gzip_js_files:
         command: ' find build/ -iname \'*.js\'  -exec bash -c \' gzip -9 -f < "{}" > "{}.gz" \' \\; '
@@ -111,9 +111,9 @@
         files: [
           { src: ['factlink.*.js'], cwd: 'build/js', dest: 'build', expand: true }
         ]
-      easyXDM:
+      postFactlinkObject:
         files: [
-          { src: ['easyXDM.js'], cwd: 'app/js/jail_iframe/libs', dest: 'build/easyXDM', expand: true }
+          { src: ['postFactlinkObject.js'], cwd: 'build/js/jail_iframe/util', dest: 'build', expand: true }
         ]

       dist:
@@ -151,6 +151,7 @@
       'build/factlink_loader_publishers.js'
     ]

+
     file_variant_funcs.forEach (file_variant_func) ->
         replacements.forEach (replacement) ->
           input_filename = file_variant_func(replacement.content_file)
@@ -164,7 +165,7 @@

   grunt.registerTask 'jail_iframe', []
   grunt.registerTask 'compile',  [
-    'clean', 'copy:build', 'copy:easyXDM', 'copy:start_stop_files', 'coffee',
+    'clean', 'copy:build',  'copy:start_stop_files', 'coffee','copy:postFactlinkObject',
     'sass', 'cssUrlEmbed', 'cssmin',
     'concat', 'uglify', 'code_inliner',
     'shell:gzip_css_files', 'shell:gzip_js_files', 'copy:dist'


add test to watch execute unit tests when a unit test file changes

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -364,6 +364,11 @@
 				]
 				options:
 					livereload: true
+			test:
+				files: './test/**/*.*'
+				tasks: [
+					'karma'
+				]
 			# Used to keep the web server alive
 			none:
 				files: 'none'


Remove unused subcommand run.

Remove unused subcommand run.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -82,8 +82,6 @@
         command: "jasmine-node --verbose --captureExceptions test"
       coverage:
         command: "istanbul cover jasmine-node --captureExceptions test && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage"
-      run:
-        command: "node lib/run.js stock.xml"
       publish:
         command: 'npm publish'


add 'dev' grunt task

add 'dev' grunt task

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -112,4 +112,5 @@

   grunt.registerTask "server", ["compile", "jasmine:dist:build", "connect"]
   grunt.registerTask "test", ["compile", "jasmine"]
-  grunt.registerTask "default", ['test', 'uglify', 'update-version', 'watch:test']
+  grunt.registerTask "dev", ["compile", "watch"]
+  grunt.registerTask "default", ['test', 'uglify', 'update-version']


make cypher queries execute in databrowser even when starting with whitespace

--- community/server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/search/CypherSearcher.coffee
+++ community/server/src/main/coffeescript/neo4j/webadmin/modules/databrowser/search/CypherSearcher.coffee
@@ -25,7 +25,7 @@
     constructor : (server) ->
       @server = server
       @urlResolver = new ItemUrlResolver(server)
-      @pattern = /// ^
+      @pattern = /// ^\s*
                     (start|cypher|create) # Start with "start", "cypher" or "create"
                     ([\s\S]+)  # followed by anything
                     $


Groups are supported / Headers on

Groups are supported / Headers on

--- SwaggerImporter.coffee
+++ SwaggerImporter.coffee
@@ -3,25 +3,25 @@
 SwaggerImporter = ->

     # Create Paw requests from a Postman Request (object)
-    @createPawRequest = (context, swaggerCollection, swaggerPath, swaggerRequest) ->
-
+    @createPawRequest = (context, swaggerCollection, swaggerRequestPath, swaggerRequestMethod, swaggerRequestValue) ->
+
+        if swaggerRequestValue.summary
+          swaggerRequestTitle = swaggerRequestValue.summary
+        else
+          swaggerRequestTitle = swaggerRequestPath
+
+        swaggerRequestUrl = swaggerCollection.schemes[0] + '://' + swaggerCollection.host + swaggerCollection.basePath + swaggerRequestPath
+        swaggerRequestMethod = swaggerRequestMethod.toUpperCase()
+
         # Create Paw request
-        for own swaggerMethod, request of swaggerRequest
+        pawRequest = context.createRequest swaggerRequestTitle, swaggerRequestMethod, swaggerRequestUrl
+
+        for index, swaggerRequestParamValue of swaggerRequestValue.parameters

-          swaggerRequestTitle = request.summary
-          swaggerRequestUrl = swaggerCollection.schemes[0] + "://" + swaggerCollection.host + swaggerCollection.basePath + swaggerPath
-          swaggerMethod = swaggerMethod.toUpperCase()
+          # Add Headers
+          if swaggerRequestParamValue.in == 'header'
+            pawRequest.setHeader swaggerRequestParamValue.name, "value"

-          pawRequest = context.createRequest swaggerRequestTitle, swaggerMethod, swaggerRequestUrl
-
-          # # Add Headers
-          # # Postman stores headers like HTTP headers, separated by \n
-          # postmanHeaders = postmanRequest["headers"].split "\n"
-          # for headerLine in postmanHeaders
-          #     match = headerLine.match /^([^\s\:]*)\s*\:\s*(.*)$/
-          #     if match
-          #         pawRequest.setHeader match[1], match[2]
-          #
           # # Set raw body
           # if postmanRequest["dataMode"] == "raw"
           #     contentType = pawRequest.getHeaderByName "Content-Type"
@@ -68,10 +68,23 @@
           #     pawRequest.multipartBody = bodyObject
           #

-          break # Should have only one key
-
         return pawRequest

+    @createPawGroup = (context, swaggerCollection, swaggerRequestPathName, swaggerRequestPathValue) ->
+
+        # Create Paw group
+        pawGroup = context.createRequestGroup swaggerRequestPathName
+
+        for own swaggerRequestMethod, swaggerRequestValue of swaggerRequestPathValue
+
+            # Create a Paw request
+            pawRequest = @createPawRequest context, swaggerCollection, swaggerRequestPathName, swaggerRequestMethod, swaggerRequestValue
+
+            # Add request to root group
+            pawGroup.appendChild pawRequest
+
+        return pawGroup
+
     @importString = (context, string) ->

         # Parse JSON collection
@@ -83,19 +96,17 @@
           throw new Error "Invalid Swagger file (not a valid JSON)"

         if swaggerCollection
+
           # Create a PawGroup
           pawRootGroup = context.createRequestGroup swaggerCollection.info.title
-
-          # Add Swagger requests in root
-          if pawRootGroup
-
-              for own path, request of swaggerCollection.paths
-
-                  # Create a Paw request
-                  pawRequest = @createPawRequest context, swaggerCollection, path, request

-                  # Add request to root group
-                  pawRootGroup.appendChild pawRequest
+          # Add Swagger groups
+          for own swaggerRequestPathName, swaggerRequestPathValue of swaggerCollection.paths
+
+            pawGroup = @createPawGroup context, swaggerCollection, swaggerRequestPathName, swaggerRequestPathValue
+
+            # Add group to root
+            pawRootGroup.appendChild pawGroup

           return true


Added polygon drawing support to Sketch.

--- src/client/sketch.coffee
+++ src/client/sketch.coffee
@@ -174,6 +174,17 @@
     @g.stroke()
     @g.closePath()

+  polygon: (points) ->
+    @g.beginPath()
+    @g.moveTo points[0].x, points[0].y
+
+    for p in points[1..]
+      @g.lineTo p.x, p.y
+
+    @g.fill() if isFillEnabled
+    @g.stroke() if isStrokeEnabled
+    @g.closePath()
+
   createImage: (width, height) ->
     imageData = @g.createImageData(width, height)


call watch() directly instead of creating a new process

--- src/commands/test.coffee
+++ src/commands/test.coffee
@@ -42,10 +42,5 @@
       process.exit if failures > 0 then 1 else 0

 module.exports = test = (options) ->
-  # TODO: call watch() directly
-  # but this somehow exits the process and
-  # doesn't wait for callbacks
-
-  child = require('child_process').fork path.join(__dirname, '..', '..', 'bin', 'brunch'), ['build']
-  child.on 'exit', ->
+  watch yes, options, ->
     new BrunchTestRunner options


:memo: Typo

:memo: Typo

--- src/config-schema.coffee
+++ src/config-schema.coffee
@@ -197,7 +197,7 @@
       softWrapAtPreferredLineLength:
         type: 'boolean'
         default: false
-        description: 'Instead of wrapping lines to the window\'s width, wrap lines to the number of characters defined by the `Preferred Line Length` setting. This will only take effect when the soft wrap config setting is enabled globally or for the current language. Note: I you like to hide the wrap guide (the vertical line) you can disable the `wrap-guide` package.'
+        description: 'Instead of wrapping lines to the window\'s width, wrap lines to the number of characters defined by the `Preferred Line Length` setting. This will only take effect when the soft wrap config setting is enabled globally or for the current language. Note: If you like to hide the wrap guide (the vertical line) you can disable the `wrap-guide` package.'
       softWrapHangingIndent:
         type: 'integer'
         default: 0


Models: Invitation: Added modifiedAt field.

--- workers/social/lib/social/models/invitation.coffee
+++ workers/social/lib/social/models/invitation.coffee
@@ -84,6 +84,9 @@
       createdAt     :
         type        : Date
         default     : -> new Date
+      modifiedAt    :
+        type        : Date
+        default     : -> new Date

   accept$: secure (client, callback) ->
     { delegate } = client.connection


oauth: throw error if no foreignAuth info is in session;
more err checking

fixes bug where if user tries to manually inititate oauth
flow when logged in (which they shouldn't), it'll throw an
error, instead of failing silently.

--- workers/social/lib/social/models/user/index.coffee
+++ workers/social/lib/social/models/user/index.coffee
@@ -452,7 +452,10 @@
     callback null, {account, replacementToken}

   @fetchUserByProvider = (provider, session, callback)->
-    {foreignAuth}                              = session
+    {foreignAuth} = session
+    unless foreignAuth
+      return callback createKodingError "No foreignAuth:#{provider} info in session"
+
     query                                      = {}
     query["foreignAuth.#{provider}.foreignId"] = foreignAuth[provider].foreignId

@@ -801,18 +804,19 @@

   @copyOauthFromSessionToUser: (username, clientId, callback)->
     JSession.one {clientId: clientId}, (err, session) =>
-      if err
-        callback err
-      else
-        if session.foreignAuth
-          {foreignAuth, foreignAuthType}          = session
-          query                                   = {}
+      if err then callback err
+      else
+        {foreignAuth, foreignAuthType} = session
+        if foreignAuth and foreignAuthType
+          query = {}
           query["foreignAuth.#{foreignAuthType}"] = foreignAuth[foreignAuthType]

-          @update {username}, $set: query, ->
-            session.update $unset: {foreignAuth: "", foreignAuthType:""}, callback
+          @update {username}, $set: query, (err)->
+            if err then callback err
+            else
+              session.update $unset: {foreignAuth: "", foreignAuthType:""}, callback
         else
-          callback()
+          callback createKodingError "No foreignAuth:#{foreignAuthType} info in session"

   @setSSHKeys: secure (client, sshKeys, callback)->
     @fetchUser client, (err,user)->


fix reference to @config (fixes #172)

fix reference to @config (fixes #172)

--- src/wow.coffee
+++ src/wow.coffee
@@ -204,7 +204,7 @@
   resetAnimation: (event) ->
   	if event.type.toLowerCase().indexOf('animationend') >= 0
   		target = event.target || event.srcElement
-  		target.className = target.className.replace(config.animateClass, '').trim()
+  		target.className = target.className.replace(@config.animateClass, '').trim()

   customStyle: (box, hidden, duration, delay, iteration) ->
     @cacheAnimationName(box) if hidden


fix photo helpers comments

fix photo helpers comments

--- server/helpers/photo.coffee
+++ server/helpers/photo.coffee
@@ -1,6 +1,6 @@

-# Orientation string generated by gm EXIF data extraction
+# Orientation strings generated by GM lib when performing EXIF data extraction.
 codes =
     TopLeft: 1
     TopRight: 2
@@ -12,7 +12,7 @@
     LeftBottom: 8

-# Helpers around photo metadata
+# Helpers to make photo metadata handling easy.
 module.exports = photoHelpers =


<debug> Adding missing cleanup method & improving conditions.

--- server/models/driver.coffee
+++ server/models/driver.coffee
@@ -114,10 +114,17 @@

 		file =
 			path : data.path
-
-		unless file.ext = path.extname(file.path) and file.ext in ['.zip', '.tar', '.tar.bz2', '.tar.gz', '.js', '.coffee']
+
+		cleanUp = ->
+			fs.unlink file.path, (err) ->
+				console.log 'Could not delete %s', file.path if err
+				cb null # loop anyway
+
+		file.ext = path.extname(file.path)
+		unless file.ext in ['.zip', '.tar', '.tar.bz2', '.tar.gz', '.js', '.coffee']
 			callback 'Unknown file extension', null
 			return cleanUp()
+
 		unless file.name = path.basename(file.path, file.ext)
 			callback 'Unknown file name', null
 			return cleanUp()


add debug info about perf

add debug info about perf

--- server/utils/logging.coffee
+++ server/utils/logging.coffee
@@ -13,12 +13,15 @@
 else 0

 lastLogs = new Array(15)
+lastDate = +new Date()
 index = -1

 addToLastLogs = ->
     index = (index + 1) % 15
     lastLogs[index] = util.format.apply this, arguments

+pad = (nb) ->
+    ((nb + 10000) + "").substring 1

 module.exports = (options) ->

@@ -27,12 +30,16 @@

     logger = (level) -> ->

+        newDate = +new Date()
+        delta = newDate - lastDate
+        lastDate = newDate

-        args = new Array arguments.length + 2
+        args = new Array arguments.length + 3
         args[0] = COLORS[level]
-        args[1] = prefix
+        args[1] = "+" + ((delta + 10000) + "").substring 1
+        args[2] = prefix
         for arg, i in arguments
-            args[i+2] = arg
+            args[i+3] = arg

         addToLastLogs.apply null, args


DevTools: Autocomplete for DevTools editors, to do that we're using the show-hint addon and customized coffeescript-hint for codemirror

--- client/DevTools/AppView.coffee
+++ client/DevTools/AppView.coffee
@@ -379,47 +379,79 @@
       then @storage.unsetKey @_lastFileKey
       else @emit "RecentFileLoaded"

+  loadAddons:(callback)->
+
+    {cdnRoot} = CollaborativeEditorPane
+
+    KodingAppsController.appendHeadElements
+      identifier : "codemirror-addons"
+      items      : [
+        {
+          type   : 'script'
+          url    : "#{cdnRoot}/addon/selection/active-line.js"
+        },
+        {
+          type   : 'style'
+          url    : "#{cdnRoot}/addon/hint/show-hint.css"
+        },
+        {
+          type   : 'script'
+          url    : "#{cdnRoot}/addon/hint/show-hint.js"
+        },
+        {
+          type   : 'script'
+          url    : "#{cdnRoot}/addon/hint/coffeescript-hint.js"
+        },
+        {
+          type   : 'script'
+          url    : "#{cdnRoot}/addon/hint/css-hint.js"
+        }
+      ]
+    , callback
+
   createEditor: (callback)->

-    {cdnRoot} = CollaborativeEditorPane
-
-    KodingAppsController.appendScriptElement 'script',
-      url        : "#{cdnRoot}/addon/selection/active-line.js"
-      identifier : "codemirror-activeline-addon"
-      callback   : =>
-
-        @codeMirrorEditor = CodeMirror @container.getDomElement()[0],
-          lineNumbers     : yes
-          lineWrapping    : yes
-          styleActiveLine : yes
-          scrollPastEnd   : yes
-          cursorHeight    : 1
-          tabSize         : 2
-          mode            : @_mode
-          extraKeys       :
-            "Cmd-S"       : @bound "handleSave"
-            "Ctrl-S"      : @bound "handleSave"
-            "Alt-R"       : => @emit "RunRequested"
-            "Shift-Ctrl-R": => @emit "AutoRunRequested"
-            "Tab"         : (cm)->
-              spaces = Array(cm.getOption("indentUnit") + 1).join " "
-              cm.replaceSelection spaces, "end", "+input"
-
-        @setEditorTheme 'blackboard'
-        @setEditorMode @_mode ? "coffee"
-
-        callback?()
-
-        @header.addSubView @info = new KDView
-          cssClass : "inline-info"
-          partial  : "saved"
-
-        @on 'EditorDidSave', =>
-          @info.updatePartial 'saved'; @info.setClass 'in'
-          KD.utils.wait 1000, => @info.unsetClass 'in'
-
-        @codeMirrorEditor.on 'focus', => @emit "FocusedOnMe"
-        @emit 'ready'
+    @loadAddons =>
+
+      @codeMirrorEditor = CodeMirror @container.getDomElement()[0],
+        lineNumbers     : yes
+        lineWrapping    : yes
+        styleActiveLine : yes
+        scrollPastEnd   : yes
+        cursorHeight    : 1
+        tabSize         : 2
+        mode            : @_mode
+        extraKeys       :
+          "Cmd-S"       : @bound "handleSave"
+          "Ctrl-S"      : @bound "handleSave"
+          "Shift-Cmd-S" : => @emit "SaveAllRequested"
+          "Shift-Ctrl-S": => @emit "SaveAllRequested"
+          "Alt-R"       : => @emit "RunRequested"
+          "Shift-Ctrl-R": => @emit "AutoRunRequested"
+          "Ctrl-Space"  : (cm)->
+            mode = CodeMirror.innerMode(cm.getMode()).mode.name
+            if mode is 'coffeescript'
+              CodeMirror.showHint cm, CodeMirror.coffeescriptHint
+            else if mode is 'css'
+              CodeMirror.showHint cm, CodeMirror.hint.css
+          "Tab"         : (cm)->
+            spaces = Array(cm.getOption("indentUnit") + 1).join " "
+            cm.replaceSelection spaces, "end", "+input"
+
+      @setEditorTheme()
+      @setEditorMode @_mode ? "coffee"
+
+      callback?()
+
+      @header.addSubView @info = new KDView
+        cssClass : "inline-info"
+        partial  : "saved"
+
+      @on 'EditorDidSave', =>
+        @info.updatePartial 'saved'; @info.setClass 'in'
+        KD.utils.wait 1000, => @info.unsetClass 'in'
+
+      @codeMirrorEditor.on 'focus', => @emit "FocusedOnMe"

   openFile: (file, content)->


Fix autocomplete hack

Fix autocomplete hack

--- client/components/Login.coffee
+++ client/components/Login.coffee
@@ -64,9 +64,9 @@
         <Grid fluid>

Вход в систему

-            <form onSubmit={@tryLogin} autocomplete="off">
-                <input type="text" style={{display:"none"}}>
-                <input type="password" style={{display:"none"}}>
+            <form onSubmit={@tryLogin} autoComplete="off">
+                <input type="text" style={{display:"none"}}/>
+                <input type="password" style={{display:"none"}}/>
                 {
                 if not @state.loading


Build: style sourcemaps commented out

Build: style sourcemaps commented out

--- client/landing/gulpfile.coffee
+++ client/landing/gulpfile.coffee
@@ -49,7 +49,7 @@
   gulp.src STYLES_PATH
     .pipe stylus
       compress  : yes
-      sourcemap : inline  : yes
+      # sourcemap : inline  : yes
     .pipe concat 'main.css'
     .pipe rename 'main.css'
     .pipe livereload()


speeling

speeling

--- client/lanes/lib/format.coffee
+++ client/lanes/lib/format.coffee
@@ -1,9 +1,9 @@
 Lanes.u.format ||= {}

-Lanes.u.format.shartDate = (d) ->
+Lanes.u.format.shortDate = (d) ->
     _.moment(d).format('L')

-Lanes.u.format.shartDateTime = (d) ->
+Lanes.u.format.shortDateTime = (d) ->
     _.moment(d).format('lll')

 Lanes.u.format.currency = (v) ->


Refresh mentions marks on route change

Refresh mentions marks on route change

--- client/routes/roomRoute.coffee
+++ client/routes/roomRoute.coffee
@@ -37,7 +37,7 @@
 			Session.set 'openedRoom', room._id

 			Session.set 'editRoomTitle', false
-			readMessage.disable()
+			RoomManager.updateMentionsMarksOfRoom type + name
 			Meteor.setTimeout ->
 				readMessage.readNow()
 			, 2000


Apply model management API changes to comparison table. PP-4

--- client/src/scripts/scoring-view.coffee
+++ client/src/scripts/scoring-view.coffee
@@ -247,7 +247,8 @@

       format4f = d3.format '.4f' # precision = 4

-      scoreWithLowestError = min scores, (score) -> score.result.metrics.error
+      #TODO what does it mean to have > 1 metrics
+      scoreWithLowestError = min scores, (score) -> (head score.result.metrics).error_measure

       inputParamsWithAlgorithm = map scores, (score) ->
         algorithm: score.model.model_algorithm
@@ -262,16 +263,17 @@

       for score, scoreIndex in scores
         model = score.model
-        metrics = score.result.metrics
-        auc = metrics.auc.members
-        cm = metrics.cm.members
+        #TODO what does it mean to have > 1 metrics
+        metrics = head score.result.metrics
+        auc = metrics.auc
+        cm = metrics.cm
         errorBadge = if scores.length > 1 and score is scoreWithLowestError then ' (Lowest)' else ''

         algorithmRow.push td model.model_algorithm
         nameRow.push td model.key
         rocCurveRow.push td 'Loading...', "roc-#{scoreIndex}"
         inputParametersRow.push td createParameterTable parameters: inputParamsByScoreIndex[scoreIndex]
-        errorRow.push td (format4f metrics.error) + errorBadge #TODO change to bootstrap badge
+        errorRow.push td (format4f metrics.error_measure) + errorBadge #TODO change to bootstrap badge
         aucRow.push td format4f auc.AUC
         thresholdCriterionRow.push td head auc.threshold_criteria
         thresholdRow.push td head auc.threshold_for_criteria
@@ -285,7 +287,8 @@
       renderRocCurves = ($element) ->
         forEach scores, (score, scoreIndex) ->
           defer ->
-            rocCurve = createRocCurve score.result.metrics.auc.members.confusion_matrices
+            #TODO what does it mean to have > 1 metrics
+            rocCurve = createRocCurve (head score.result.metrics).auc.confusion_matrices
             $("#roc-#{scoreIndex}", $element).empty().append rocCurve
         return


JSTextView update

JSTextView update

--- JSKit/04_JSTextView.coffee
+++ JSKit/04_JSTextView.coffee
@@ -86,7 +86,7 @@

 		else # 編集不可モード

-			@setUserInteractionEnabled(false)
+			#@setUserInteractionEnabled(false)
 			if (@_editable == true) # モード変更前が編集可能モード
 				@_text = $(@_viewSelector+"_textarea").val()
 				text = JSEscape(@_text)


Add specs for begin/end patterns with nested patterns.

--- spec/app/parser-spec.coffee
+++ spec/app/parser-spec.coffee
@@ -11,46 +11,70 @@
     plist.parseString coffee_plist, (err, grammar) ->
       parser = new Parser(grammar[0])

-  describe ".getLineTokens(line, state)", ->
-    describe "when the state is omitted (start state)", ->
-      describe "when the entire line matches a single pattern with no capture groups", ->
-        it "returns a single token with the correct scope", ->
-          {tokens, state} = parser.getLineTokens("return")
+  describe ".getLineTokens(line, currentRule)", ->
+    describe "when the entire line matches a single pattern with no capture groups", ->
+      it "returns a single token with the correct scope", ->
+        {tokens, currentRule} = parser.getLineTokens("return")

-          expect(tokens.length).toBe 1
-          [token] = tokens
-          expect(token.scopes).toEqual ['source.coffee', 'keyword.control.coffee']
+        expect(tokens.length).toBe 1
+        [token] = tokens
+        expect(token.scopes).toEqual ['source.coffee', 'keyword.control.coffee']

-      describe "when the entire line matches a single pattern with capture groups", ->
-        it "returns a single token with the correct scope", ->
-          {tokens, state} = parser.getLineTokens("new foo.bar.Baz")
+    describe "when the entire line matches a single pattern with capture groups", ->
+      it "returns a single token with the correct scope", ->
+        {tokens, currentRule} = parser.getLineTokens("new foo.bar.Baz")

-          expect(tokens.length).toBe 3
-          [newOperator, whitespace, className] = tokens
-          expect(newOperator).toEqual value: 'new', scopes: ['source.coffee', 'meta.class.instance.constructor', 'keyword.operator.new.coffee']
-          expect(whitespace).toEqual value: ' ', scopes: ['source.coffee', 'meta.class.instance.constructor']
-          expect(className).toEqual value: 'foo.bar.Baz', scopes: ['source.coffee', 'meta.class.instance.constructor', 'entity.name.type.instance.coffee']
+        expect(tokens.length).toBe 3
+        [newOperator, whitespace, className] = tokens
+        expect(newOperator).toEqual value: 'new', scopes: ['source.coffee', 'meta.class.instance.constructor', 'keyword.operator.new.coffee']
+        expect(whitespace).toEqual value: ' ', scopes: ['source.coffee', 'meta.class.instance.constructor']
+        expect(className).toEqual value: 'foo.bar.Baz', scopes: ['source.coffee', 'meta.class.instance.constructor', 'entity.name.type.instance.coffee']

-      describe "when the line matches multiple patterns", ->
-        it "returns multiple tokens, filling in regions that don't match patterns with tokens in the grammar's global scope", ->
-          {tokens, state} = parser.getLineTokens(" return new foo.bar.Baz ")
+    describe "when the line matches multiple patterns", ->
+      it "returns multiple tokens, filling in regions that don't match patterns with tokens in the grammar's global scope", ->
+        {tokens, currentRule} = parser.getLineTokens(" return new foo.bar.Baz ")

-          expect(tokens.length).toBe 7
+        expect(tokens.length).toBe 7

-          expect(tokens[0]).toEqual value: ' ', scopes: ['source.coffee']
-          expect(tokens[1]).toEqual value: 'return', scopes: ['source.coffee', 'keyword.control.coffee']
-          expect(tokens[2]).toEqual value: ' ', scopes: ['source.coffee']
-          expect(tokens[3]).toEqual value: 'new', scopes: ['source.coffee', 'meta.class.instance.constructor', 'keyword.operator.new.coffee']
-          expect(tokens[4]).toEqual value: ' ', scopes: ['source.coffee', 'meta.class.instance.constructor']
-          expect(tokens[5]).toEqual value: 'foo.bar.Baz', scopes: ['source.coffee', 'meta.class.instance.constructor', 'entity.name.type.instance.coffee']
-          expect(tokens[6]).toEqual value: ' ', scopes: ['source.coffee']
+        expect(tokens[0]).toEqual value: ' ', scopes: ['source.coffee']
+        expect(tokens[1]).toEqual value: 'return', scopes: ['source.coffee', 'keyword.control.coffee']
+        expect(tokens[2]).toEqual value: ' ', scopes: ['source.coffee']
+        expect(tokens[3]).toEqual value: 'new', scopes: ['source.coffee', 'meta.class.instance.constructor', 'keyword.operator.new.coffee']
+        expect(tokens[4]).toEqual value: ' ', scopes: ['source.coffee', 'meta.class.instance.constructor']
+        expect(tokens[5]).toEqual value: 'foo.bar.Baz', scopes: ['source.coffee', 'meta.class.instance.constructor', 'entity.name.type.instance.coffee']
+        expect(tokens[6]).toEqual value: ' ', scopes: ['source.coffee']

-      describe "when the line matches a begin and end pattern", ->
-        it "returns tokens based on the beginCaptures, endCaptures and the child scope", ->
-          {tokens, state} = parser.getLineTokens("'''single-quoted heredoc'''")
+    describe "when the line matches a begin/end pattern", ->
+      it "returns tokens based on the beginCaptures, endCaptures and the child scope", ->
+        {tokens, currentRule} = parser.getLineTokens("'''single-quoted heredoc'''")

-          expect(tokens.length).toBe 3
+        expect(tokens.length).toBe 3

-          expect(tokens[0]).toEqual value: "'''", scopes: ['source.coffee', 'string.quoted.heredoc.coffee', 'punctuation.definition.string.begin.coffee']
-          expect(tokens[1]).toEqual value: "single-quoted heredoc", scopes: ['source.coffee', 'string.quoted.heredoc.coffee']
-          expect(tokens[2]).toEqual value: "'''", scopes: ['source.coffee', 'string.quoted.heredoc.coffee', 'punctuation.definition.string.end.coffee']
+        expect(tokens[0]).toEqual value: "'''", scopes: ['source.coffee', 'string.quoted.heredoc.coffee', 'punctuation.definition.string.begin.coffee']
+        expect(tokens[1]).toEqual value: "single-quoted heredoc", scopes: ['source.coffee', 'string.quoted.heredoc.coffee']
+        expect(tokens[2]).toEqual value: "'''", scopes: ['source.coffee', 'string.quoted.heredoc.coffee', 'punctuation.definition.string.end.coffee']
+
+    describe "when begin/end pattern spans multiple lines", ->
+      it "uses the currentRule returned by the first line to parse the second line", ->
+        {tokens: firstTokens, currentRule} = parser.getLineTokens("'''single-quoted")
+        {tokens: secondTokens, currentRule} = parser.getLineTokens("heredoc'''", currentRule)
+
+        expect(firstTokens.length).toBe 2
+        expect(secondTokens.length).toBe 2
+
+        expect(firstTokens[0]).toEqual value: "'''", scopes: ['source.coffee', 'string.quoted.heredoc.coffee', 'punctuation.definition.string.begin.coffee']
+        expect(firstTokens[1]).toEqual value: "single-quoted", scopes: ['source.coffee', 'string.quoted.heredoc.coffee']
+
+        expect(secondTokens[0]).toEqual value: "heredoc", scopes: ['source.coffee', 'string.quoted.heredoc.coffee']
+        expect(secondTokens[1]).toEqual value: "'''", scopes: ['source.coffee', 'string.quoted.heredoc.coffee', 'punctuation.definition.string.end.coffee']
+
+   describe "when the line matches a begin/end pattern that contains sub-patterns", ->
+     it "returns tokens within the begin/end scope based on the sub-patterns", ->
+       {tokens, currentRule} = parser.getLineTokens('"""heredoc with character escape \\t"""')
+
+       expect(tokens.length).toBe 4
+
+       expect(tokens[0]).toEqual value: '"""', scopes: ['source.coffee', 'string.quoted.double.heredoc.coffee', 'punctuation.definition.string.begin.coffee']
+       expect(tokens[1]).toEqual value: "heredoc with character escape ", scopes: ['source.coffee', 'string.quoted.double.heredoc.coffee']
+       expect(tokens[2]).toEqual value: "\\t", scopes: ['source.coffee', 'string.quoted.double.heredoc.coffee', 'constant.character.escape.coffee']
+       expect(tokens[3]).toEqual value: '"""', scopes: ['source.coffee', 'string.quoted.double.heredoc.coffee', 'punctuation.definition.string.end.coffee']


fix polling for health model

fix polling for health model

--- lib/sensu-dashboard/assets/javascripts/models/health.coffee
+++ lib/sensu-dashboard/assets/javascripts/models/health.coffee
@@ -43,8 +43,7 @@

     startLongPolling: (intervalSeconds) =>
       @longPolling = true
-      if intervalSeconds
-        @intervalSeconds = @intervalSeconds
+      @intervalSeconds = intervalSeconds if intervalSeconds
       @executeLongPolling()

     stopLongPolling: =>
@@ -56,4 +55,4 @@
           @onFetch()

     onFetch: =>
-      setTimeout(@executeLongPolling, 10000) if @longPolling
+      setTimeout(@executeLongPolling, 1000 * @intervalSeconds) if @longPolling


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/drawable.coffee
+++ src/drawable.coffee
@@ -48,10 +48,12 @@
   height: 5

 player.bind "draw", (canvas) ->
-  canvas.fillColor("white")
-
   # Text will be drawn positioned relatively to the object.
-  canvas.fillText("Hey, drawing stuff is pretty easy.", 5, 5)
+  canvas.drawText
+    text: "Hey, drawing stuff is pretty easy."
+    color: "white"
+    x: 5
+    y: 5
 </pre></code>

 @name draw


standard stepsize to 1

standard stepsize to 1

--- src/g/zoomer.coffee
+++ src/g/zoomer.coffee
@@ -10,4 +10,4 @@
     textVisible: true
     labelLength: 20
     labelFontsize: "10px"
-    stepSize: 2
+    stepSize: 1


bin file update

bin file update

--- JSKit/02_JSView.coffee
+++ JSKit/02_JSView.coffee
@@ -347,6 +347,8 @@

 		$(@_viewSelector).bind
 			'touchstart mousedown':=>
+				if (@_editable == false)
+					event.preventDefault()
 				@touched = true
 				if (typeof @touchesBegan == 'function')
 					if (isTouch)
@@ -356,6 +358,8 @@
 						e = event
 					@touchesBegan(e)
 			'touchmove mousemove':=>
+				if (@_editable == false)
+					event.preventDefault()
 				if (@touched)
 					if (typeof @touchesMoved == 'function')
 						if (isTouch)
@@ -365,6 +369,8 @@
 							e = event
 						@touchesMoved(e)
 			'touchend mouseup':=>
+				if (@_editable == false)
+					event.preventDefault()
 				@touched = false
 				if (typeof @touchesEnded == 'function')
 					if (isTouch)


added copy_files task

added copy_files task

--- Jamesfile.coffee
+++ Jamesfile.coffee
@@ -8,6 +8,10 @@
 coffeeify = require 'coffeeify'

 mkdirp = require 'mkdirp'
+
+copyFile = (file) -> james.read(file).write(file.replace('client/', 'public/'))
+
+james.task 'copy_files', -> james.list('client/images/*').forEach copyFile

 BROWSERIFY_OPTS =
   debug: true
@@ -34,6 +38,7 @@
     .transform(stylus)
     .write(file
       .replace('client/stylus', 'public/css')
+      .replace('.stylus', '.css')
       .replace('.styl', '.css'))

 james.task 'stylus', ->
@@ -43,6 +48,7 @@
   james.watch 'client/coffee/*.coffee', -> james.run 'browserify'
   james.watch 'client/jade/*.jade', (ev, file) -> transmogrifyJade file
   james.watch 'client/stylus/*.styl', (ev, file) -> transmogrifyStylus file
+  james.watch 'client/images/*', (ev, file) -> copyFile file

-james.task 'default', ['browserify', 'jade_static', 'stylus']
+james.task 'default', ['browserify', 'jade_static', 'stylus', 'copy_files']
 james.task 'watch', ['default', 'actual_watch']


clean up livereload middleware config

clean up livereload middleware config

changes where options are searched for to top-level "livereload" to
facilitate easy sharing of config with the grunt-watch-nospawn task.

--- tasks/server.coffee
+++ tasks/server.coffee
@@ -28,8 +28,6 @@
     apiProxyPrefix  = grunt.config.get("server.apiProxy.prefix") || undefined
     apiProxyHost = grunt.config.get("server.apiProxy.host") || "localhost"
     apiProxyChangeOrigin = grunt.config.get("server.apiProxy.changeOrigin") || true
-    liveReloadEnabled = grunt.config.get("server.liveReload.enabled")
-    liveReloadPort = grunt.config.get("server.liveReload.port")
     webPort = process.env.WEB_PORT || grunt.config.get("server.web.port") || 8000
     webRoot = grunt.config.get("server.base") || "generated"
     staticRoutes = grunt.config.get("server.staticRoutes")
@@ -37,6 +35,7 @@
     pushStateEnabled = grunt.config.get("server.pushState")
     relativeUrlRoot = grunt.config.get("server.relativeUrlRoot")
     @requiresConfig("server.apiProxy.prefix") if pushStateEnabled and apiProxyEnabled
+
     app = express()
     server = http.createServer(app)

@@ -44,13 +43,7 @@

     app.configure ->
       app.use(express.compress())
-
-      if liveReloadEnabled
-        grunt.log.writeln("Connected LiveReload middleware on port: #{liveReloadPort}")
-        app.use(require('connect-livereload')(
-          port: liveReloadPort
-        ))
-
+      configureLiveReloadMiddleware(app)
       app.use(express.static("#{process.cwd()}/#{webRoot}"))
       mountUserStaticRoutes(app, webRoot, staticRoutes)

@@ -81,6 +74,13 @@
     grunt.log.writeln("Mounting application at path '#{relativeUrlRoot}'.")
     _(express()).tap (otherApp) ->
       otherApp.use(relativeUrlRoot, app)
+
+  configureLiveReloadMiddleware = (app) ->
+    return unless livereload = grunt.config.get("livereload")
+    return if livereload.injectScript == false
+    port = livereload.port || 35729
+    grunt.log.writeln("Injecting LiveReload script into HTML documents (port: #{port})")
+    app.use(require('connect-livereload')({port}))

   mountUserStaticRoutes = (app, webRoot, staticRoutes) ->
     _(staticRoutes).each (src, dest) ->


avoid a race condition by an existential check

--- client/landing/app/CommonViews/messagesList.coffee
+++ client/landing/app/CommonViews/messagesList.coffee
@@ -101,7 +101,10 @@
     {group} = @snapshot

     # Cleanup my activities on my content
-    myid = KD.whoami()._id
+    myid = KD.whoami()?.getId()
+
+    return  unless myid
+
     group = (member for member in group when member.id isnt myid)

     @participants = new options.linkGroupClass {group}


added test cases for en-route A1-international shipment

--- test/a1.coffee
+++ test/a1.coffee
@@ -19,14 +19,25 @@

     describe "in transit package", ->

-    before (done) ->
-      fs.readFile 'test/stub_data/a1_shipping.xml', 'utf8', (err, xmlDoc) ->
-        _a1Client.presentResponse xmlDoc, 'trk', (err, resp) ->
-          should.not.exist(err)
-          _package = resp
-          console.log "[A1 INTL RESP] #{JSON.stringify resp}"
-          done()
+      before (done) ->
+        fs.readFile 'test/stub_data/a1_shipping.xml', 'utf8', (err, xmlDoc) ->
+          _a1Client.presentResponse xmlDoc, 'trk', (err, resp) ->
+            should.not.exist(err)
+            _package = resp
+            console.log "[A1 INTL RESP] #{JSON.stringify resp}"
+            done()

-    describe "delivered package", ->
-      it "works", ->
+      it "has a status of en-route", ->
+        expect(_package.status).to.equal ShipperClient.STATUS_TYPES.EN_ROUTE

+      it "has a destination of Chicago, IL", ->
+        expect(_package.destination).to.equal 'Chicago, IL, 60607'
+
+      it "has 1 activity", ->
+        expect(_package.activities).to.have.length 1
+
+      it "has first activity with timestamp, location and details", ->
+        act = _package.activities[0]
+        expect(act.timestamp).to.deep.equal new Date 'Jul 10 2015 10:10:00'
+        expect(act.details).to.equal 'Shipment has left seller facility and is in transit'
+        expect(act.location).to.equal 'Whitestown, IN 46075'


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/bounded.coffee
+++ test/bounded.coffee
@@ -5,7 +5,7 @@

   obj.include Bounded

-
+  ok obj.distance

 test "#bounds returns correct x, y, width, height", ->
   x = 5


Correct ps object structure

Correct ps object structure

--- test/cavalry.coffee
+++ test/cavalry.coffee
@@ -46,17 +46,19 @@
   it "should fetch a ps object", (done) ->
     server.on "request", (req, res) ->
       res.end JSON.stringify
-        id: 'someID'
-        status: 'running'
-        repo: 'reponame'
-        commit: 'commitid'
-        cwd: '/dev/null'
-        drone: 'testDrone'
+        someID:
+          id: 'someID'
+          status: 'running'
+          repo: 'reponame'
+          commit: 'commitid'
+          cwd: '/dev/null'
+          drone: 'testDrone'
     cavalry.ps 'cavalry-us', (err, procs) ->
       done assert.deepEqual procs,
-        id: 'someID'
-        status: 'running'
-        repo: 'reponame'
-        commit: 'commitid'
-        cwd: '/dev/null'
-        drone: 'testDrone'
+        someID:
+          id: 'someID'
+          status: 'running'
+          repo: 'reponame'
+          commit: 'commitid'
+          cwd: '/dev/null'
+          drone: 'testDrone'


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/durable.coffee
+++ test/durable.coffee
@@ -9,9 +9,9 @@
   5.times ->
     active = obj.update()

-  equals active, true, "object is active until duration is exceeded"
+  equals obj.I.active, true, "object is active until duration is exceeded"

   6.times ->
     active = obj.update()

-  equals active, false, "object is inactive after duration"
+  equals obj.I.active, false, "object is inactive after duration"


another heap of tests

another heap of tests

--- test/lru-map.coffee
+++ test/lru-map.coffee
@@ -367,9 +367,27 @@
 			expect(lmap.currentSize()).to.be 0

 	describe '#set()', ->
-		it 'reaps stale entries'
-		it 'errors if calcSize does not return a positive number'
-		it 'errors if the value cannot fit'
+		it 'reaps stale entries', ->
+			lmap = new LRUMap
+			called = false
+			lmap.reapStale = -> called = true
+			lmap.set 'hello', 'world'
+
+			expect(called).to.be true
+
+		it 'errors if calcSize does not return a positive number', ->
+			lmap = new LRUMap calcSize: (x) -> x.size
+
+			expect(->
+				lmap.set 'negative', {size: -1}
+			).to.throwError /positive/
+
+		it 'errors if the value cannot fit', ->
+			lmap = new LRUMap {maxSize: 10, calcSize: (x) -> x.size}
+
+			expect(->
+				lmap.set 'big', {size: 100}
+			).to.throwError /size/

 		it 'updates the current size on insert', ->
 			lmap = new LRUMap {calcSize: (x) -> x.size}
@@ -391,27 +409,217 @@
 			lmap.set 'one', {size: 10}
 			expect(lmap.currentSize()).to.be 10

-		it 'sets the specified key to the specified value'
-		it 'returns the map'
-		it 'causes eviction when appropriate'
+		it 'sets the specified key to the specified value', ->
+			lmap = new LRUMap
+
+			# test the primitive cases
+			lmap.set true, 1234
+			lmap.set 'hello', null
+			lmap.set 56, 'sup'
+
+			expect(lmap.get true).to.be 1234
+			expect(lmap.get 'hello').to.be null
+			expect(lmap.get 56).to.be 'sup'
+
+			# build a non-trivial object key to test ref eq
+			objKey = {foo: 'bar', quux: {frank: new Date}}
+			objKey.baz = objKey
+
+			# also use a non-trivial value
+			objValue = objKey.quux
+
+			lmap.set objKey, objValue
+			expect(lmap.get objKey).to.be objValue
+
+			# finally verify that non-primitives aren't compared w/ value eq
+			objKey2 = {hey: 'how are ya'}
+			lmap.set objKey2, {i: 'dunno lol'}
+
+			expect(lmap.get {hey: 'how are ya'}).to.not.be {i: 'dunno lol'}
+			expect(lmap.get objKey2).to.not.be {i: 'dunno lol'}
+
+		it 'returns the map', ->
+			lmap = new LRUMap
+			expect(lmap.set('foo', 'bar')).to.be lmap
+
+		it 'causes eviction when appropriate', ->
+			lmap = new LRUMap {maxSize: 10, calcSize: (x) -> x.size}
+			lmap.set 'seven', {size: 7}
+			lmap.set 'six', {size: 6}
+
+			expect(lmap.get 'seven').to.be undefined
+			expect(lmap.get 'six').to.eql {size: 6}

 		describe 'eviction', ->
-			it 'evicts the oldest entries'
-			it 'updates the current size'
-			it 'reaps stales'
-			it 'triggers onEvict'
-			it 'triggers onRemove'
+			it 'evicts the oldest entries', ->
+				lmap = new LRUMap {maxSize: 10, calcSize: (x) -> x.size}
+				lmap.set 'one', {size: 3}
+				lmap.set 'two', {size: 3}
+				lmap.set 'three', {size: 3}
+				lmap.get 'one'
+				lmap.set 'four', {size: 3}
+
+				expect(lmap.get 'one').to.eql {size: 3}
+				expect(lmap.get 'two').to.be undefined
+				expect(lmap.get 'three').to.eql {size: 3}
+				expect(lmap.get 'four').to.eql {size: 3}
+
+			it 'updates the current size', ->
+				lmap = new LRUMap {maxSize: 10, calcSize: (x) -> x.size}
+				lmap.set 'one', {size: 3}
+				expect(lmap.currentSize()).to.be 3
+				lmap.set 'two', {size: 4}
+				expect(lmap.currentSize()).to.be 7
+				lmap.set 'three', {size: 5}
+				expect(lmap.currentSize()).to.be 9
+				lmap.set 'four', {size: 6}
+				expect(lmap.currentSize()).to.be 6
+
+			it 'reaps stales', ->
+				lmap = new LRUMap
+				called = false
+				lmap.reapStale = -> called = true
+				lmap.set 'hello', 'world'
+
+				expect(called).to.be true
+
+			it 'triggers onEvict correctly', ->
+				called = 0
+
+				lmap = new LRUMap {
+					maxSize: 5
+					calcSize: (x) -> x.size
+				}
+
+				lmap.set 'five', {size: 5}
+
+				lmap.onEvict (k, v) ->
+					called++
+					expect(k).to.be 'five'
+					expect(v.size).to.be 5
+
+				lmap.set 'one', {size: 1}
+				expect(called).to.be 1
+
+				lmap.onEvict -> expect().fail('should not have evicted')
+				lmap.set 'two', {size: 2}
+				lmap.set 'alsotwo', {size: 2}
+
+				lmap.onEvict (k, v) ->
+					unless k in ['one', 'two']
+						expect().fail 'wrong entry evicted'
+
+					if k is 'one'
+						expect(v.size).to.be 1
+					else
+						expect(v.size).to.be 2
+
+					called++
+
+				lmap.set 'three', {size: 3}
+				expect(called).to.be 3
+
+			it 'triggers onRemove', ->
+				called = 0
+
+				lmap = new LRUMap {
+					maxSize: 5
+					calcSize: (x) -> x.size
+				}
+
+				lmap.set 'five', {size: 5}
+
+				lmap.onRemove (k, v) ->
+					called++
+					expect(k).to.be 'five'
+					expect(v.size).to.be 5
+
+				lmap.set 'one', {size: 1}
+				expect(called).to.be 1
+
+				lmap.onRemove -> expect().fail('should not have evicted')
+				lmap.set 'two', {size: 2}
+				lmap.set 'alsotwo', {size: 2}
+
+				lmap.onRemove (k, v) ->
+					unless k in ['one', 'two']
+						expect().fail 'wrong entry evicted'
+
+					if k is 'one'
+						expect(v.size).to.be 1
+					else
+						expect(v.size).to.be 2
+
+					called++
+
+				lmap.set 'three', {size: 3}
+				expect(called).to.be 3

 	describe '#delete()', ->
-		it 'removes the specified key and its value'
-		it 'updates the current size'
-		it 'reaps stale entries after deleting the specified key'
-		it 'returns true if the key existed'
-		it 'returns false if the key did not exist'
+		it 'removes the specified key and its value', ->
+			lmap = new LRUMap
+			lmap.set 'one', 'yessir'
+			expect(lmap.get 'one').to.be 'yessir'
+
+			lmap.delete 'one'
+			expect(lmap.get 'one').to.be undefined
+
+			objKey = {foo: 'bar'}
+			objKey.baz = objKey
+
+			lmap.set objKey, 'here i am'
+			expect(lmap.get objKey).to.be 'here i am'
+
+			lmap.delete objKey
+			expect(lmap.get objKey).to.be undefined
+
+		it 'updates the current size', ->
+			lmap = new LRUMap calcSize: (x) -> x.size
+			lmap.set 'one', {size: 1}
+			lmap.set 'five', {size: 5}
+			lmap.set 'ten', {size: 10}
+			expect(lmap.currentSize()).to.be 16
+
+			lmap.delete 'five'
+			expect(lmap.currentSize()).to.be 11
+			lmap.delete 'ten'
+			expect(lmap.currentSize()).to.be 1
+			lmap.delete 'one'
+			expect(lmap.currentSize()).to.be 0
+
+		it 'reaps stale entries after deleting the specified key', ->
+			lmap = new LRUMap
+			called = 0
+			lmap.reapStale = -> called++
+			lmap.set 'hello', 'world'
+			lmap.delete 'hello'
+			lmap.delete 'hello'
+			lmap.delete 'nope'
+			expect(called).to.be 4
+
+		it 'returns true iff the key existed', ->
+			lmap = new LRUMap
+			lmap.set 'hello', 'world'
+			expect(lmap.delete 'nope').to.be false
+			expect(lmap.delete 'hello').to.be true
+			expect(lmap.delete 'hello').to.be false

 	describe '#clear()', ->
-		it 'removes all entries'
-		it 'updates the current size'
+		it 'removes all entries', ->
+			lmap = new LRUMap
+			lmap.set 'one', 1
+			lmap.set 'two', 2
+
+			lmap.clear()
+			lmap.forEach -> expect().fail 'should have no entries'
+
+		it 'updates the current size', ->
+			lmap = new LRUMap
+			lmap.set 'one', 1
+			lmap.set 'two', 2
+
+			lmap.clear()
+			expect(lmap.currentSize()).to.be 0

 	describe '#get()', ->
 		it 'returns undefined if the key does not exist'


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/metered.coffee
+++ test/metered.coffee
@@ -5,5 +5,7 @@
   obj.include(Metered)

   ok obj.meter
+
+test

 module()


moveItems as predefinedable option for slider proto view

--- app/assets/javascripts/ultimate/backbone/views/slider.js.coffee
+++ app/assets/javascripts/ultimate/backbone/views/slider.js.coffee
@@ -1,5 +1,6 @@
 # TODO timeout without cycling, maybe bouce effect
 # TODO mousewheel
+# TODO touch events

 Ultimate.Backbone.Views ||= {}

@@ -23,11 +24,11 @@
   cycling: false
   interval: 0
   durationPerPixel: 2
+  moveItems: 0

   totalItems: 0
   itemSize: 0
   displayItems: 0
-  moveItems: 0
   overItems: 0
   prevItems: 0 # can as setting

@@ -46,7 +47,7 @@
       needSize = @totalItems * @itemSize
       @jLine[@sizeAttr](needSize)  if @jLine[@sizeAttr]() < needSize
       @displayItems = Math.round(@jDisplay[@sizeAttr]() / @itemSize)
-      @moveItems = Math.round(@displayItems / 2)
+      @moveItems ||= Math.round(@displayItems / 2)
       @overItems = @totalItems - @displayItems
       if @overItems
         if @prevItems


remove unwanted wording

remove unwanted wording


Working on gulp buil

Working on gulp buil

--- Gulpfile.coffee
+++ Gulpfile.coffee
@@ -340,11 +340,11 @@
     [__, currentBranch] = stdout.match /\*\s*(.+)\s/
     return console.error("Unable to retrieve current branch") unless currentBranch?
     tag = args[1] or currentBranch
-    git.checkout tag, ->
+    git.checkout tag, quiet: yes, ->
       tasks.clean()
       COMPILATION_MODE = DEBUG_MODE
       tasks.compile().then ->
-        git.checkout currentBranch, ->
+        git.checkout currentBranch, quiet: yes, ->
           cb()


fix linting errors

fix linting errors

Signed-off-by: Dominik Richter <b7323add517e187efe0283b76511b7ed762bfe13@gmail.com>

--- coffee/controllers/main.coffee
+++ coffee/controllers/main.coffee
@@ -75,9 +75,9 @@
       limit = opts.limit || 50
       # construct a query for pagination
       query = _.merge(
-          {page: page, limit: limit},
-          ( opts.query || {} )
-        )
+        {page: page, limit: limit},
+        ( opts.query || {} )
+      )
       # fetch the field
       Restangular.all(field).getList(query).then(
         # success handling
@@ -87,7 +87,8 @@
           # if we got a result and want to continue
           # then try fetching the next page
           if not stop_when(data)
-            o = _.merge((opts.query || {}), {'stop_when':stop_when, 'page':page + 1, 'limit': limit })
+            o = _.merge( (opts.query || {}),
+              {'stop_when':stop_when, 'page':page + 1, 'limit': limit })
             fetchAllPaginated(field, action, o)
         # error handling
         , () ->


anable load 25 over projects

anable load 25 over projects

--- coffee/services/redmine.coffee
+++ coffee/services/redmine.coffee
@@ -173,10 +173,13 @@
   ###
    Load projects on url
   ###
-  loadProjects: (success, error) ->
+  loadProjects: (success, error, params) ->
+    params = params or {}
+    params.limit = params.limit or 100
     config =
       method: "GET"
       url: @auth.url + "/projects.json"
+      params: params
     config = @setBasicConfig config, @auth
     @$http(config)
       .success( (data, status, headers, config) =>


fix scope

fix scope

--- coffee/services/redmine.coffee
+++ coffee/services/redmine.coffee
@@ -1,6 +1,428 @@
 timeTracker.factory "Redmine", ($http, $q, Base64, Ticket, Project, Analytics, Log, State, Const) ->

   _redmines = {}
+
+  class Redmine
+
+    @CONTENT_TYPE: "application/json"
+    @AJAX_TIME_OUT: 30 * 1000
+    @LIMIT_MAX: 100
+
+    constructor: (@auth, @$http, @$q, @Ticket, @Project, @Base64, @Analytics, @Log, @State, @Const) ->
+      @url = auth.url
+
+    _timeEntryData:
+      "time_entry":
+        "issue_id": 0
+        "hours": 0
+        "activity_id": 8
+        "comments": ""
+
+    getIssuesCanceler: null
+
+
+    ###
+     convert json to xml.
+    ###
+    @JSONtoXML: (obj, depth) ->
+      result = ""
+      indent = ""
+      depth = depth || 0
+
+      i = depth
+      while --i > 0 then indent += "  "
+      for key, val of obj
+        name = key
+        if key.match(/^\d+$/) then name = "item"
+        if typeof(val) is "object"
+          result += indent + "<" + name + ">\n"
+          depth++
+          result += @JSONtoXML(val, depth)
+          depth--
+          result += indent + "</" + name + ">\n"
+        else
+          val = '' + val
+          val = val.replace(/&/g, "&").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'")
+          result += indent + "<" + name + ">" + val + "</" + name + ">\n"
+
+      return result
+
+
+    ###
+     bind log.
+    ###
+    _bindDefer: (success, error, mothodName) ->
+      deferred = @$q.defer()
+      onSuccess = (args...) =>
+        args[0].account = @auth
+        deferred.resolve(args...)
+        success?(args...)
+
+      onError = (args...) =>
+        @Log.warn(mothodName + " failed:\taccount: " + @auth.name + "\tstatus: " + args[1])
+        if not args[0] or args[0].isBlank?()
+          args[0] = { error: true }
+        args[0] = Object.merge(args[0], {account: @auth, status: args[1]})
+        deferred.reject(args...)
+        error?(args...)
+
+      return success: onSuccess, error: onError, promise: deferred.promise
+
+
+    ###
+     set basic configs for $http.
+    ###
+    _setBasicConfig: (config, auth) ->
+      config.headers = "Content-Type": Redmine.CONTENT_TYPE
+      config.timeout = config.timeout or Redmine.AJAX_TIME_OUT
+      if auth.apiKey? and auth.apiKey.length > 0
+        @$http.defaults.headers.common["X-Redmine-API-Key"] = auth.apiKey
+      else
+        delete @$http.defaults.headers.common["X-Redmine-API-Key"]
+        @$http.defaults.headers.common['Authorization'] = 'Basic ' + @Base64.encode(auth.id + ':' + auth.pass)
+      return config
+
+
+    ###
+     load issues.
+    ###
+    _getIssues: (params, success, error) ->
+      params.limit = params.limit or Redmine.LIMIT_MAX
+      deferred = @$q.defer()
+      config =
+        method: "GET"
+        url: @auth.url + "/issues.json"
+        params: params
+        timeout: deferred.promise
+      config = @_setBasicConfig config, @auth
+      @State.isLoadingIssue = true
+      @$http(config)
+        .success((data, status, headers, config) =>
+          data.params = params
+          data.url    = @auth.url
+          data.issues = data.issues or []
+          data.issues = for issue in data.issues
+            issue.text    = issue.subject
+            issue.show    = @Const.SHOW.DEFAULT
+            issue.url     = @auth.url
+            issue.total   = issue.spent_hours or 0
+            @Ticket.create(issue)
+          deferred.resolve(data)
+          success?(data))
+        .error((args...) =>
+          deferred.reject(args...)
+          error?(args...))
+        .finally(() =>
+          return if @getIssuesCanceler isnt deferred
+          @State.isLoadingIssue = false)
+      return deferred
+
+
+    ###
+     load issues.
+    ###
+    getIssues: (params, success, error) ->
+      if @getIssuesCanceler
+        @getIssuesCanceler.resolve()
+        @getIssuesCanceler = null
+      o = @_bindDefer(success, error, "getIssues")
+      @getIssuesCanceler = @_getIssues(params, o.success, o.error)
+      @getIssuesCanceler.promise
+
+
+    ###
+     load issues pararell.
+    ###
+    getIssuesPararell: (params, success, error) ->
+      o = @_bindDefer(success, error, "getIssuesPararell")
+      @_getIssues(params, o.success, o.error).promise
+
+
+    ###
+     load issues from `start` to `end`.
+    ###
+    getIssuesRange: (params, start, end, success, error) ->
+      params = params or {}
+      params.limit = Redmine.LIMIT_MAX
+      pages = Math.ceil((end - start) / Redmine.LIMIT_MAX)
+      promises = [1..pages].map (n) =>
+        params = Object.clone(params)
+        params.offset = start + (n - 1) * Redmine.LIMIT_MAX
+        if params.offset + Redmine.LIMIT_MAX > end
+          params.limit = end - params.offset
+        @_getIssues(params).promise
+
+      r = @_bindDefer(success, error, "getIssuesRange")
+      @$q.all(promises)
+        .then((dataAry) ->
+          data = dataAry.reduce((a, b) -> a.issues.add(b.issues);a)
+          r.success(data)
+        , (data) -> r.error(data))
+        .finally(() => @State.isLoadingIssue = false)
+
+      return r.promise
+
+
+    ###
+     Load tickets associated to user ID.
+    ###
+    getIssuesOnUser: (success, error) ->
+      params =
+        assigned_to_id: @auth.userId
+      o = @_bindDefer(success, error, "getIssuesOnUser")
+      @_getIssues(params, o.success, o.error).promise
+
+
+    ###
+     get any ticket using id.
+    ###
+    getIssuesById: (issueId, success, error) ->
+      config =
+        method: "GET"
+        url: @auth.url + "/issues/#{issueId}.json"
+      config = @_setBasicConfig config, @auth
+      @$http(config)
+        .success((data, status, headers, config) =>
+          @Log.debug("getIssuesById: success")
+          @Log.debug data
+          if data?.issue?
+            data.issue.text  = data.issue.subject
+            data.issue.total = data.issue.spent_hours or 0
+            data.issue.show  = @Const.SHOW.DEFAULT
+            data.issue.url   = @auth.url
+            data.issue       = @Ticket.create(data.issue)
+          success?(data.issue, status, headers, config))
+        .error((data, status, headers, config) =>
+          @Log.debug("getIssuesById: error on #{issueId}")
+          @Log.debug data
+          issue = @Ticket.create(url: @auth.url, id:  issueId)
+          if status isnt @Const.NOT_FOUND or status isnt @Const.UNAUTHORIZED
+            @Analytics.sendException("Error: getIssuesById")
+          error?(issue, status))
+
+
+    ###
+     submit time entry to redmine server.
+    ###
+    submitTime: (config, success, error) ->
+      success = success or @Const.NULLFUNC
+      error = error or @Const.NULLFUNC
+      @_timeEntryData.time_entry.issue_id    = config.issueId
+      @_timeEntryData.time_entry.hours       = config.hours
+      @_timeEntryData.time_entry.comments    = config.comment
+      @_timeEntryData.time_entry.activity_id = config.activityId
+      config =
+        method: "POST"
+        url: @auth.url + "/issues/#{@_timeEntryData.time_entry.issue_id}/time_entries.json"
+        data: Redmine.JSONtoXML @_timeEntryData
+      config = @_setBasicConfig config, @auth
+      config.headers = "Content-Type": "application/xml"
+      @$http(config)
+        .success((args...) =>
+          @Log.info("Time Entry Posted.\t account:#{@auth.name}\tid:#{@_timeEntryData.time_entry.issue_id}\thours:#{@_timeEntryData.time_entry.hours}")
+          @Analytics.sendEvent 'timeEntry', 'post', 'success', 1
+          success(args...))
+        .error((args...) =>
+          @Log.info("Time Entry Post Failed.\t account:#{@auth.name}\tid:#{@_timeEntryData.time_entry.issue_id}\thours:#{@_timeEntryData.time_entry.hours}")
+          @Log.debug args
+          @Analytics.sendEvent 'timeEntry', 'post', 'failed', 1
+          @Analytics.sendException("Error: submitTime\tstatus: " + args[1])
+          error(args...))
+
+
+    ###
+     laod time entry. uses promise.
+    ###
+    loadTimeEntries: (params) ->
+      r = @_bindDefer(null, null, "loadTimeEntries")
+      params = params or {}
+      params.limit = params.limit or Redmine.LIMIT_MAX
+      config =
+        method: "GET"
+        url: @auth.url + "/time_entries.json"
+        params: params
+      config = @_setBasicConfig config, @auth
+      @$http(config)
+        .success((args...) =>
+          args[0].url = @auth.url
+          r.success(args[0]))
+        .error(r.error)
+      return r.promise
+
+
+    ###
+     Load projects on url
+    ###
+    loadProjects: (params) =>
+      @Log.debug "loadProjects start"
+      r = @_bindDefer(null, null, "loadProjects")
+
+      params = params or {}
+      params.limit = params.limit or Redmine.LIMIT_MAX
+      config =
+        method: "GET"
+        url: @auth.url + "/projects.json"
+        params: params
+      config = @_setBasicConfig config, @auth
+      @$http(config)
+        .success((data, status, headers, config) =>
+          if data?.projects?
+            data.projects = for prj in data.projects
+              newPrj =
+                url: @auth.url,
+                id: prj.id,
+                text: prj.name,
+                show: @Const.SHOW.DEFAULT
+              @Project.create(newPrj)
+            @Log.groupCollapsed "redmine.loadProjects()"
+            @Log.table data.projects
+            @Log.groupEnd "redmine.loadProjects()"
+          r.success(data, status))
+        .error(r.error)
+
+      return r.promise
+
+
+    ###
+     load projects from `start` to `end`.
+    ###
+    loadProjectsRange: (params, start, end) =>
+      params = params or {}
+      end = end or Redmine.LIMIT_MAX
+      params.limit = Redmine.LIMIT_MAX
+      pages = Math.ceil((end - start) / Redmine.LIMIT_MAX)
+      promises = [1..pages].map (n) =>
+        params = Object.clone(params)
+        params.offset = start + (n - 1) * Redmine.LIMIT_MAX
+        if params.offset + Redmine.LIMIT_MAX > end
+          params.limit = end - params.offset
+        return @loadProjects(params)
+
+      @$q.all(promises)
+        .then((d) ->
+          return d.reduce((a, b) -> a.projects.add(b.projects); a)
+        , (d) -> return d)
+
+
+    ###
+     load projects using id.
+    ###
+    loadProjectById: (id) ->
+      @Log.debug "loadProjectById start"
+      r = @_bindDefer(null, null, "loadProjectById")
+      config =
+        method: "GET"
+        url: @auth.url + "/projects/#{id}.json"
+      config = @_setBasicConfig config, @auth
+      @$http(config)
+        .success((data, status, headers, config) =>
+          if data?.project?
+            newPrj =
+              url:  @auth.url,
+              id:   data.project.id,
+              text: data.project.name,
+              show: @Const.SHOW.DEFAULT
+            data.project = @Project.create(newPrj)
+            @Log.groupCollapsed "redmine.loadProjectById()"
+            @Log.table data.project
+            @Log.groupEnd "redmine.loadProjectById()"
+          r.success(data, status))
+        .error((data, status, headers, config) =>
+          data = targetId: id if data.isBlank()
+          r.error(data, status))
+      return r.promise
+
+
+    ###
+     Load user on url associated to auth.
+    ###
+    _getUser: (success, error) ->
+      error = error or @Const.NULLFUNC
+      config =
+        method: "GET"
+        url: @auth.url + "/users/current.json?include=memberships"
+      config = @_setBasicConfig config, @auth
+      @$http(config)
+        .success((data, status, headers, config) =>
+          if not data.user or not data.user.id
+            error(data, status, headers, config)
+            return
+          @auth.userId = data.user.id
+          success(data, status, headers, config))
+        .error(error)
+
+
+    ###
+     find user recursive.
+    ###
+    _findUser: (success, error, args...) ->
+      @auth.url = @auth.url.substring(0, @auth.url.lastIndexOf('/'))
+      if not @auth.url.match(/^https?:\/\/.+/)
+        error(args...)
+        return
+      @_getUser(success, (msg...) => @_findUser(success, error, msg...))
+
+
+    ###
+     find user from url.
+    ###
+    findUser: (success, error) ->
+      @auth.url = @auth.url + '/'
+      if not @auth.url.isUrl()
+        error(null, @Const.URL_FORMAT_ERROR)
+        return
+      @_findUser((data) =>
+          data.account = @auth
+          success(data)
+        , error or @Const.NULLFUNC)
+
+
+    ###
+     Load time entry activities.
+    ###
+    loadActivities: (success, error) ->
+      r = @_bindDefer(success, error, "loadActivities")
+      config =
+        method: "GET"
+        url: @auth.url + "/enumerations/time_entry_activities.json"
+      config = @_setBasicConfig config, @auth
+      @$http(config)
+        .success(r.success)
+        .error(r.error)
+      return r.promise
+
+
+    ###
+     laod queries. uses promise.
+    ###
+    loadQueries: (params) ->
+      r = @_bindDefer(null, null, "loadQueries")
+      params = params or {}
+      params.limit = params.limit or Redmine.LIMIT_MAX
+      config =
+        method: "GET"
+        url: @auth.url + "/queries.json"
+        params: params
+      config = @_setBasicConfig config, @auth
+      @$http(config)
+        .success(r.success)
+        .error(r.error)
+      return r.promise
+
+    ###
+     laod status. uses promise.
+    ###
+    loadStatuses: () ->
+      r = @_bindDefer(null, null, "loadStatuses")
+      config =
+        method: "GET"
+        url: @auth.url + "/issue_statuses.json"
+      config = @_setBasicConfig config, @auth
+      @$http(config)
+        .success(r.success)
+        .error(r.error)
+      return r.promise
+

   return {

@@ -28,424 +450,3 @@
         delete _redmines[auth.url]

   }
-
-class Redmine
-
-  @CONTENT_TYPE: "application/json"
-  @AJAX_TIME_OUT: 30 * 1000
-  @LIMIT_MAX: 100
-
-  constructor: (@auth, @$http, @$q, @Ticket, @Project, @Base64, @Analytics, @Log, @State, @Const) ->
-    @url = auth.url
-
-  _timeEntryData:
-    "time_entry":
-      "issue_id": 0
-      "hours": 0
-      "activity_id": 8
-      "comments": ""
-
-  getIssuesCanceler: null
-
-
-  ###
-   convert json to xml.
-  ###
-  @JSONtoXML: (obj, depth) ->
-    result = ""
-    indent = ""
-    depth = depth || 0
-
-    i = depth
-    while --i > 0 then indent += "  "
-    for key, val of obj
-      name = key
-      if key.match(/^\d+$/) then name = "item"
-      if typeof(val) is "object"
-        result += indent + "<" + name + ">\n"
-        depth++
-        result += @JSONtoXML(val, depth)
-        depth--
-        result += indent + "</" + name + ">\n"
-      else
-        val = '' + val
-        val = val.replace(/&/g, "&").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'")
-        result += indent + "<" + name + ">" + val + "</" + name + ">\n"
-
-    return result
-
-
-  ###
-   bind log.
-  ###
-  _bindDefer: (success, error, mothodName) ->
-    deferred = @$q.defer()
-    onSuccess = (args...) =>
-      args[0].account = @auth
-      deferred.resolve(args...)
-      success?(args...)
-
-    onError = (args...) =>
-      @Log.warn(mothodName + " failed:\taccount: " + @auth.name + "\tstatus: " + args[1])
-      if not args[0] or args[0].isBlank?()
-        args[0] = { error: true }
-      args[0] = Object.merge(args[0], {account: @auth, status: args[1]})
-      deferred.reject(args...)
-      error?(args...)
-
-    return success: onSuccess, error: onError, promise: deferred.promise
-
-
-  ###
-   set basic configs for $http.
-  ###
-  _setBasicConfig: (config, auth) ->
-    config.headers = "Content-Type": Redmine.CONTENT_TYPE
-    config.timeout = config.timeout or Redmine.AJAX_TIME_OUT
-    if auth.apiKey? and auth.apiKey.length > 0
-      @$http.defaults.headers.common["X-Redmine-API-Key"] = auth.apiKey
-    else
-      delete @$http.defaults.headers.common["X-Redmine-API-Key"]
-      @$http.defaults.headers.common['Authorization'] = 'Basic ' + @Base64.encode(auth.id + ':' + auth.pass)
-    return config
-
-
-  ###
-   load issues.
-  ###
-  _getIssues: (params, success, error) ->
-    params.limit = params.limit or Redmine.LIMIT_MAX
-    deferred = @$q.defer()
-    config =
-      method: "GET"
-      url: @auth.url + "/issues.json"
-      params: params
-      timeout: deferred.promise
-    config = @_setBasicConfig config, @auth
-    @State.isLoadingIssue = true
-    @$http(config)
-      .success((data, status, headers, config) =>
-        data.params = params
-        data.url    = @auth.url
-        data.issues = data.issues or []
-        data.issues = for issue in data.issues
-          issue.text    = issue.subject
-          issue.show    = @Const.SHOW.DEFAULT
-          issue.url     = @auth.url
-          issue.total   = issue.spent_hours or 0
-          @Ticket.create(issue)
-        deferred.resolve(data)
-        success?(data))
-      .error((args...) =>
-        deferred.reject(args...)
-        error?(args...))
-      .finally(() =>
-        return if @getIssuesCanceler isnt deferred
-        @State.isLoadingIssue = false)
-    return deferred
-
-
-  ###
-   load issues.
-  ###
-  getIssues: (params, success, error) ->
-    if @getIssuesCanceler
-      @getIssuesCanceler.resolve()
-      @getIssuesCanceler = null
-    o = @_bindDefer(success, error, "getIssues")
-    @getIssuesCanceler = @_getIssues(params, o.success, o.error)
-    @getIssuesCanceler.promise
-
-
-  ###
-   load issues pararell.
-  ###
-  getIssuesPararell: (params, success, error) ->
-    o = @_bindDefer(success, error, "getIssuesPararell")
-    @_getIssues(params, o.success, o.error).promise
-
-
-  ###
-   load issues from `start` to `end`.
-  ###
-  getIssuesRange: (params, start, end, success, error) ->
-    params = params or {}
-    params.limit = Redmine.LIMIT_MAX
-    pages = Math.ceil((end - start) / Redmine.LIMIT_MAX)
-    promises = [1..pages].map (n) =>
-      params = Object.clone(params)
-      params.offset = start + (n - 1) * Redmine.LIMIT_MAX
-      if params.offset + Redmine.LIMIT_MAX > end
-        params.limit = end - params.offset
-      @_getIssues(params).promise
-
-    r = @_bindDefer(success, error, "getIssuesRange")
-    @$q.all(promises)
-      .then((dataAry) ->
-        data = dataAry.reduce((a, b) -> a.issues.add(b.issues);a)
-        r.success(data)
-      , (data) -> r.error(data))
-      .finally(() => @State.isLoadingIssue = false)
-
-    return r.promise
-
-
-  ###
-   Load tickets associated to user ID.
-  ###
-  getIssuesOnUser: (success, error) ->
-    params =
-      assigned_to_id: @auth.userId
-    o = @_bindDefer(success, error, "getIssuesOnUser")
-    @_getIssues(params, o.success, o.error).promise
-
-
-  ###
-   get any ticket using id.
-  ###
-  getIssuesById: (issueId, success, error) ->
-    config =
-      method: "GET"
-      url: @auth.url + "/issues/#{issueId}.json"
-    config = @_setBasicConfig config, @auth
-    @$http(config)
-      .success((data, status, headers, config) =>
-        @Log.debug("getIssuesById: success")
-        @Log.debug data
-        if data?.issue?
-          data.issue.text  = data.issue.subject
-          data.issue.total = data.issue.spent_hours or 0
-          data.issue.show  = @Const.SHOW.DEFAULT
-          data.issue.url   = @auth.url
-          data.issue       = @Ticket.create(data.issue)
-        success?(data.issue, status, headers, config))
-      .error((data, status, headers, config) =>
-        @Log.debug("getIssuesById: error on #{issueId}")
-        @Log.debug data
-        issue = @Ticket.create(url: @auth.url, id:  issueId)
-        if status isnt @Const.NOT_FOUND or status isnt @Const.UNAUTHORIZED
-          @Analytics.sendException("Error: getIssuesById")
-        error?(issue, status))
-
-
-  ###
-   submit time entry to redmine server.
-  ###
-  submitTime: (config, success, error) ->
-    success = success or @Const.NULLFUNC
-    error = error or @Const.NULLFUNC
-    @_timeEntryData.time_entry.issue_id    = config.issueId
-    @_timeEntryData.time_entry.hours       = config.hours
-    @_timeEntryData.time_entry.comments    = config.comment
-    @_timeEntryData.time_entry.activity_id = config.activityId
-    config =
-      method: "POST"
-      url: @auth.url + "/issues/#{@_timeEntryData.time_entry.issue_id}/time_entries.json"
-      data: Redmine.JSONtoXML @_timeEntryData
-    config = @_setBasicConfig config, @auth
-    config.headers = "Content-Type": "application/xml"
-    @$http(config)
-      .success((args...) =>
-        @Log.info("Time Entry Posted.\t account:#{@auth.name}\tid:#{@_timeEntryData.time_entry.issue_id}\thours:#{@_timeEntryData.time_entry.hours}")
-        @Analytics.sendEvent 'timeEntry', 'post', 'success', 1
-        success(args...))
-      .error((args...) =>
-        @Log.info("Time Entry Post Failed.\t account:#{@auth.name}\tid:#{@_timeEntryData.time_entry.issue_id}\thours:#{@_timeEntryData.time_entry.hours}")
-        @Log.debug args
-        @Analytics.sendEvent 'timeEntry', 'post', 'failed', 1
-        @Analytics.sendException("Error: submitTime\tstatus: " + args[1])
-        error(args...))
-
-
-  ###
-   laod time entry. uses promise.
-  ###
-  loadTimeEntries: (params) ->
-    r = @_bindDefer(null, null, "loadTimeEntries")
-    params = params or {}
-    params.limit = params.limit or Redmine.LIMIT_MAX
-    config =
-      method: "GET"
-      url: @auth.url + "/time_entries.json"
-      params: params
-    config = @_setBasicConfig config, @auth
-    @$http(config)
-      .success((args...) =>
-        args[0].url = @auth.url
-        r.success(args[0]))
-      .error(r.error)
-    return r.promise
-
-
-  ###
-   Load projects on url
-  ###
-  loadProjects: (params) =>
-    @Log.debug "loadProjects start"
-    r = @_bindDefer(null, null, "loadProjects")
-
-    params = params or {}
-    params.limit = params.limit or Redmine.LIMIT_MAX
-    config =
-      method: "GET"
-      url: @auth.url + "/projects.json"
-      params: params
-    config = @_setBasicConfig config, @auth
-    @$http(config)
-      .success((data, status, headers, config) =>
-        if data?.projects?
-          data.projects = for prj in data.projects
-            newPrj =
-              url: @auth.url,
-              id: prj.id,
-              text: prj.name,
-              show: @Const.SHOW.DEFAULT
-            @Project.create(newPrj)
-          @Log.groupCollapsed "redmine.loadProjects()"
-          @Log.table data.projects
-          @Log.groupEnd "redmine.loadProjects()"
-        r.success(data, status))
-      .error(r.error)
-
-    return r.promise
-
-
-  ###
-   load projects from `start` to `end`.
-  ###
-  loadProjectsRange: (params, start, end) =>
-    params = params or {}
-    end = end or Redmine.LIMIT_MAX
-    params.limit = Redmine.LIMIT_MAX
-    pages = Math.ceil((end - start) / Redmine.LIMIT_MAX)
-    promises = [1..pages].map (n) =>
-      params = Object.clone(params)
-      params.offset = start + (n - 1) * Redmine.LIMIT_MAX
-      if params.offset + Redmine.LIMIT_MAX > end
-        params.limit = end - params.offset
-      return @loadProjects(params)
-
-    @$q.all(promises)
-      .then((d) ->
-        return d.reduce((a, b) -> a.projects.add(b.projects); a)
-      , (d) -> return d)
-
-
-  ###
-   load projects using id.
-  ###
-  loadProjectById: (id) ->
-    @Log.debug "loadProjectById start"
-    r = @_bindDefer(null, null, "loadProjectById")
-    config =
-      method: "GET"
-      url: @auth.url + "/projects/#{id}.json"
-    config = @_setBasicConfig config, @auth
-    @$http(config)
-      .success((data, status, headers, config) =>
-        if data?.project?
-          newPrj =
-            url:  @auth.url,
-            id:   data.project.id,
-            text: data.project.name,
-            show: @Const.SHOW.DEFAULT
-          data.project = @Project.create(newPrj)
-          @Log.groupCollapsed "redmine.loadProjectById()"
-          @Log.table data.project
-          @Log.groupEnd "redmine.loadProjectById()"
-        r.success(data, status))
-      .error((data, status, headers, config) =>
-        data = targetId: id if data.isBlank()
-        r.error(data, status))
-    return r.promise
-
-
-  ###
-   Load user on url associated to auth.
-  ###
-  _getUser: (success, error) ->
-    error = error or @Const.NULLFUNC
-    config =
-      method: "GET"
-      url: @auth.url + "/users/current.json?include=memberships"
-    config = @_setBasicConfig config, @auth
-    @$http(config)
-      .success((data, status, headers, config) =>
-        if not data.user or not data.user.id
-          error(data, status, headers, config)
-          return
-        @auth.userId = data.user.id
-        success(data, status, headers, config))
-      .error(error)
-
-
-  ###
-   find user recursive.
-  ###
-  _findUser: (success, error, args...) ->
-    @auth.url = @auth.url.substring(0, @auth.url.lastIndexOf('/'))
-    if not @auth.url.match(/^https?:\/\/.+/)
-      error(args...)
-      return
-    @_getUser(success, (msg...) => @_findUser(success, error, msg...))
-
-
-  ###
-   find user from url.
-  ###
-  findUser: (success, error) ->
-    @auth.url = @auth.url + '/'
-    if not @auth.url.isUrl()
-      error(null, @Const.URL_FORMAT_ERROR)
-      return
-    @_findUser((data) =>
-        data.account = @auth
-        success(data)
-      , error or @Const.NULLFUNC)
-
-
-  ###
-   Load time entry activities.
-  ###
-  loadActivities: (success, error) ->
-    r = @_bindDefer(success, error, "loadActivities")
-    config =
-      method: "GET"
-      url: @auth.url + "/enumerations/time_entry_activities.json"
-    config = @_setBasicConfig config, @auth
-    @$http(config)
-      .success(r.success)
-      .error(r.error)
-    return r.promise
-
-
-  ###
-   laod queries. uses promise.
-  ###
-  loadQueries: (params) ->
-    r = @_bindDefer(null, null, "loadQueries")
-    params = params or {}
-    params.limit = params.limit or Redmine.LIMIT_MAX
-    config =
-      method: "GET"
-      url: @auth.url + "/queries.json"
-      params: params
-    config = @_setBasicConfig config, @auth
-    @$http(config)
-      .success(r.success)
-      .error(r.error)
-    return r.promise
-
-  ###
-   laod status. uses promise.
-  ###
-  loadStatuses: () ->
-    r = @_bindDefer(null, null, "loadStatuses")
-    config =
-      method: "GET"
-      url: @auth.url + "/issue_statuses.json"
-    config = @_setBasicConfig config, @auth
-    @$http(config)
-      .success(r.success)
-      .error(r.error)
-    return r.promise


Shell: added top-level media interface.

Shell: added top-level media interface.

--- coffee/src/shells/shell.coffee
+++ coffee/src/shells/shell.coffee
@@ -33,8 +33,21 @@
     @property 'shellid'
     @shellid @module.id

-  # disable Backbone's sync functionality
-  sync: => NotSupportedError 'Backbone::sync'
+    @properties [
+      'title',          # title of the piece of media
+      'description',    # description of the piece of media
+      'sources'         # contributing sources to the piece of media;
+                        # expected to be of type Array
+    ]
+
+
+  # -- model interface --
+  # To be overriden and implemented by inheriting classes
+
+  # Returns a URL that points to the thumbnail
+  # to be used for this piece of media.
+  thumbnail: =>
+

   # -- factory constructors --

@@ -51,16 +64,24 @@
     new shellClass.Model _.clone data

+  # -- unsupported --
+
+  # disable Backbone's sync functionality
+  sync: => NotSupportedError 'Backbone::sync'
+
+
 # register convenience construction functions globally.
 acorn.shellWithAcorn = Shell.Model.withAcorn
 acorn.shellWithData = Shell.Model.withData

+
+# Shell.MediaView -- top level media interface.
+# ---------------------------------------------
+
 class Shell.MediaView extends athena.lib.View

   className: @classNameExtend 'shell-content-view'
-
-  controls: []

   initialize: =>
     super
@@ -68,10 +89,63 @@
     unless @options.model
       acorn.errors.MissingParameterError 'Shell.MediaView', 'model'

+  controls: []
+
+  # -- media interface --
+  # To be overriden and implemented by inheriting classes
+
+  # Transition between play and pause states.
+  play: =>
+  pause: =>
+
+  # Returns true if the media view is in 'play' state, false otherwise
+  isPlaying: => false
+
+  # Seek to the provided playback offset.
+  seek: (offset) =>
+
+  # Returns the current seek offset.
+  seekOffset: => 0
+
+  # Returns the view's total duration in seconds
+  duration: => 0
+
+  # Sets the media view's volume.
+  volume: => 0
+  setVolume: (volume) =>
+
+  # Dimensions
+  # The functions below expect and return dimensions in the same
+  # format used to specify dimensions in CSS3. (e.g. 100%, 100px, etc.)
+  width: => '100%'
+  height: => '100%'
+  setWidth: (width) =>
+  setHeight: (height) =>
+
+  # Object Fit
+  # * contain: if you have set an explicit height and width on a replaced
+  #            element, object-fit:contain will cause the content to be resized
+  #            so that it is fully displayed with its intrinsic aspec ratio
+  #            preserved, but still fits inside the dimensions set for the
+  #            element.
+  #
+  # * fill:    causes the element's content to expand to completely fill the
+  #            dimensions set for it, even if this does change its intrinsic
+  #            aspect ratio.
+  #
+  # * cover:   preserves the content's intrinsic aspect ratio but alters its
+  #            width and height to completely cover the element. The smaller
+  #            of the two is made to fit the elemnt exactly, and the larger of
+  #            the two overflows the element
+  #
+  # * none:    the content's intrinsic dimensions are used.
+  objectFit: => 'contain'
+  setObjectFit: (objectFit) =>
+

 # Shell.RemixView -- uniform view to edit shell data.
-# --------------------------------------------------
+# ---------------------------------------------------

 class Shell.RemixView extends athena.lib.View


new configuration format for punchdrunk

new configuration format for punchdrunk

--- coffeescript/punchdrunk.coffee
+++ coffeescript/punchdrunk.coffee
@@ -1,5 +1,9 @@
 class @Punchdrunk
-  constructor: (game_root = "lua", punchdrunk_root = "./js") ->
+  constructor: (config = {}) ->
+    game_root = config["game_root"] or "lua"
+    punchdrunk_root = config["punchdrunk_root"] or "./js"
+    element = config["canvas"] or null
+
     # Forward print() messages to the console
     shine.stdout.write = () ->
       console.log.apply(console, arguments)
@@ -16,7 +20,7 @@
       conf_env.love.conf.call(null, conf)

       Love.root = game_root
-      love = new Love(conf.window, conf.modules)
+      love = new Love(element, conf.window, conf.modules)

       vm = new shine.VM({
         love: love


Test only against 10 categories

Test only against 10 categories

--- src/spec/integration/categories.spec.coffee
+++ src/spec/integration/categories.spec.coffee
@@ -25,8 +25,8 @@
   beforeEach (done) ->
     @client = new SphereClient config: Config

-    debug 'Creating 50 categories'
-    Promise.all _.map [1..50], => @client.categories.save(newCategory())
+    debug 'Creating 10 categories'
+    Promise.all _.map [1..10], => @client.categories.save(newCategory())
     .then (results) ->
       debug "Created #{results.length} categories"
       done()
@@ -60,7 +60,7 @@
         debug 'No category found, skipping...'
         Promise.resolve()
     .then (results) ->
-      expect(results.length).toBe 50
+      expect(results.length).toBe 10
       done()
     .catch (error) -> done(_.prettify(error))
   , 120000 # 2min


Remove id from game header

Remove id from game header

--- resources/assets/coffee/react/mp-history/game-header.coffee
+++ resources/assets/coffee/react/mp-history/game-header.coffee
@@ -45,7 +45,6 @@
         className: 'mp-history-game__header-overlay'

       div className: 'mp-history-game__stats-box',
-        span className: 'mp-history-game__stat', "##{@props.game.id.toLocaleString()}"
         span className: 'mp-history-game__stat', timeString
         span className: 'mp-history-game__stat', Lang.get "beatmaps.mode.#{@props.game.mode}"
         span className: 'mp-history-game__stat', Lang.get "multiplayer.game.scoring-type.#{@props.game.scoring_type}"


Read JSON environment file into KONFIG_JSON environment variable

Signed-off-by: Sonmez Kartal <d2d15c3d37396a5f2a09f8b42cf5f27d43a3fbde@koding.com>

--- config/generateShellEnv.coffee
+++ config/generateShellEnv.coffee
@@ -11,6 +11,9 @@
     return  if typeof node is 'object'
     add "KONFIG_#{path.join('_')}".toUpperCase(), node

+  add 'ENV_JSON_FILE', "$KONFIG_PROJECTROOT/#{options.envFileName}.json"
+  add 'KONFIG_JSON', '$(cat $ENV_JSON_FILE)'
+
   add 'KITE_HOME', '$KONFIG_KITEHOME'
   add 'GOPATH', '$KONFIG_PROJECTROOT/go'
   add 'GOBIN', '$GOPATH/bin'


Make width and padding easily adjustable

--- extras/Studio.framer/app.coffee
+++ extras/Studio.framer/app.coffee
@@ -35,7 +35,11 @@

 		# When this is set to true, the sheet won't hide on resize
 		# This is triggered by a manual open / close action
-		@fixed = false
+
+		@options =
+			padding: 20
+			width: 250
+			fixed: false

 		@render()
 		@_startListening()
@@ -56,9 +60,8 @@
 	# Render main sheet
 	_renderSheet: ->
 		@sheet = new Layer
-			width: 250
-			x: 10
-			y: 10
+			width: @options.width
+			point: 10
 			borderRadius: 4
 			backgroundColor: "#FFF"
 			style:
@@ -107,7 +110,7 @@
 			width: 16
 			height: 24
 			x: Align.center()
-			y: 25
+			y: 24
 			image: "images/logo.png"

 		ctaSlogan = new ShareLayer
@@ -132,7 +135,7 @@
 	_renderInfo: ->
 		@info = new ShareLayer
 			parent: @sheet
-			width: @sheet.width-40
+			width: @sheet.width - (@options.padding * 2)
 			y: @cta.maxY + 22
 			x: 20

@@ -256,11 +259,11 @@

 		# Toggle sheet when clicked on close or open buttons
 		@close.onClick =>
-			@fixed = true
+			@options.fixed = true
 			@_closeSheet()

 		@open.onClick =>
-			@fixed = true
+			@options.fixed = true
 			@_openSheet()

 		# When the window resizes evaluate if the sheet needs to be hidden
@@ -275,7 +278,7 @@
 	_updateHeight: ->
 		@credentials.height = @credentials.contentFrame().height
 		@info.height = @info.contentFrame().height
-		@sheet.height = @sheet.contentFrame().height + 21
+		@sheet.height = @sheet.contentFrame().height + @options.padding

 	_closeSheet: ->
 		@sheet.visible = false


remove unused @kiteController property

remove unused @kiteController property

--- client/app/MainApp/filetree/controllers/findercontroller.coffee
+++ client/app/MainApp/filetree/controllers/findercontroller.coffee
@@ -22,7 +22,6 @@

     super options, data

-    @kiteController = @getSingleton('kiteController')
     @treeController = new NFinderTreeController treeOptions, []

     if options.useStorage


Treat mount path / only as a fallback

Treat mount path / only as a fallback

--- client/finder/lib/filetree/controllers/nfindercontroller.coffee
+++ client/finder/lib/filetree/controllers/nfindercontroller.coffee
@@ -107,6 +107,8 @@
     unless machine.status.state is Machine.State.Running
       return kd.warn "Machine '#{machine.getName()}' was not ready, I skipped it."

+    options.mountPath = null  if options.mountPath is '/'
+
     { uid } = machine
     mRoots  = (@appStorage.getValue 'machineRoots') or {}
     path    = options.mountPath or mRoots[uid] or '/'


Remove debug

Remove debug

--- spec/ComponentLoader.coffee
+++ spec/ComponentLoader.coffee
@@ -246,7 +246,6 @@
       l.components.Subgraph = file
       l.getSource 'Subgraph', (err, src) ->
         return done err if err
-        console.log src
         chai.expect(src.code).to.not.be.empty
         chai.expect(src.language).to.equal 'json'
         done()
@@ -257,7 +256,6 @@
         l.components.Subgraph2 = graph
         l.getSource 'Subgraph2', (err, src) ->
           return done err if err
-          console.log src
           chai.expect(src.code).to.not.be.empty
           chai.expect(src.language).to.equal 'json'
           done()


Add a failing test for #28

Add a failing test for #28

i think it fails for the right reason! we'll see soon enough.

--- spec/powershell-spec.coffee
+++ spec/powershell-spec.coffee
@@ -216,7 +216,7 @@

     describe "Comparison operator keywords", ->
       comparisonOperators = [
-        "-eq", "-ceq", "-ieq", "-lt", "-gt", "-le", "-ge", "-ne", "-notlike",
+        "-eq", "-lt", "-gt", "-le", "-ge", "-ne", "-notlike",
         "-like", "-match", "-notmatch", "-contains", "-notcontains", "-in",
         "-notin", "-replace"
       ]
@@ -230,6 +230,14 @@
         for operator in comparisonOperators
           {tokens} = grammar.tokenizeLine operator.toUpperCase()
           expect(tokens[0]).toEqual value: operator.toUpperCase(), scopes: ["source.powershell","keyword.operator.comparison.powershell"]
+
+      it "tokenizes comparison operators when prepended with a case sensitivity marker", ->
+        for operator in comparisonOperators
+          {tokens} = grammar.tokenizeLine operator.replace('-', '-i')
+          expect(tokens[0]).toHaveScopes ["source.powershell","keyword.operator.comparison.powershell"]
+
+          {tokens} = grammar.tokenizeLine operator.replace('-', '-c')
+          expect(tokens[0]).toHaveScopes ["source.powershell","keyword.operator.comparison.powershell"]

       it "will not tokenize the operators if there's more characters", ->
         for operator in comparisonOperators


Rename .write() to writeSync()

Rename .write() to writeSync()

--- spec/status-bar-spec.coffee
+++ spec/status-bar-spec.coffee
@@ -59,7 +59,7 @@
   describe "when the buffer content has changed from the content on disk", ->
     it "disables the buffer modified indicator on save", ->
       filePath = "/tmp/atom-whitespace.txt"
-      fsUtils.write(filePath, "")
+      fsUtils.writeSync(filePath, "")
       rootView.open(filePath)
       expect(statusBar.bufferModified.text()).toBe ''
       editor.insertText("\n")
@@ -132,21 +132,21 @@
     beforeEach ->
       filePath = require.resolve('fixtures/git/working-dir/file.txt')
       newPath = path.join(fsUtils.resolveOnLoadPath('fixtures/git/working-dir'), 'new.txt')
-      fsUtils.write(newPath, "I'm new here")
+      fsUtils.writeSync(newPath, "I'm new here")
       ignoredPath = path.join(fsUtils.resolveOnLoadPath('fixtures/git/working-dir'), 'ignored.txt')
-      fsUtils.write(ignoredPath, 'ignored.txt')
+      fsUtils.writeSync(ignoredPath, 'ignored.txt')
       git.getPathStatus(filePath)
       git.getPathStatus(newPath)
       originalPathText = fsUtils.read(filePath)
       rootView.attachToDom()

     afterEach ->
-      fsUtils.write(filePath, originalPathText)
+      fsUtils.writeSync(filePath, originalPathText)
       fsUtils.remove(newPath) if fsUtils.exists(newPath)
       fsUtils.remove(ignoredPath) if fsUtils.exists(ignoredPath)

     it "displays the modified icon for a changed file", ->
-      fsUtils.write(filePath, "i've changed for the worse")
+      fsUtils.writeSync(filePath, "i've changed for the worse")
       git.getPathStatus(filePath)
       rootView.open(filePath)
       expect(statusBar.gitStatusIcon).toHaveClass('modified-status-icon')
@@ -164,16 +164,16 @@
       expect(statusBar.gitStatusIcon).toHaveClass('ignored-status-icon')

     it "updates when a status-changed event occurs", ->
-      fsUtils.write(filePath, "i've changed for the worse")
+      fsUtils.writeSync(filePath, "i've changed for the worse")
       git.getPathStatus(filePath)
       rootView.open(filePath)
       expect(statusBar.gitStatusIcon).toHaveClass('modified-status-icon')
-      fsUtils.write(filePath, originalPathText)
+      fsUtils.writeSync(filePath, originalPathText)
       git.getPathStatus(filePath)
       expect(statusBar.gitStatusIcon).not.toHaveClass('modified-status-icon')

     it "displays the diff stat for modified files", ->
-      fsUtils.write(filePath, "i've changed for the worse")
+      fsUtils.writeSync(filePath, "i've changed for the worse")
       git.getPathStatus(filePath)
       rootView.open(filePath)
       expect(statusBar.gitStatusIcon).toHaveText('+1,-1')