Improve error and warning messages

Improve error and warning messages

--- src/smc-webapp/project_files.cjsx
+++ src/smc-webapp/project_files.cjsx
@@ -379,8 +379,12 @@
     # TODO: Make better help text
     render_help_alert : ->
         last_folder_index = @props.file_search?.lastIndexOf('/')
+        if @props.file_search?.indexOf('\\') != -1
+            <Alert style={marginTop: '10px', fontWeight : 'bold'} bsStyle='danger'>
+                Warning: \ is an illegal character
+            </Alert>
         # Non-empty search and there is a file divisor ('/')
-        if @props.file_search?.length > 0 and last_folder_index > 0
+        else if @props.file_search?.length > 0 and last_folder_index > 0
             <Alert style={marginTop: '10px'} bsStyle='info'>
                 {@render_help_text(last_folder_index)}
             </Alert>
@@ -1356,14 +1360,14 @@
     displayName : 'ProjectFiles-ProjectFilesSearch'

     propTypes :
-        file_search   : rtypes.string
-        current_path  : rtypes.string
-        actions       : rtypes.object.isRequired
-        create_file   : rtypes.func.isRequired
-        create_folder : rtypes.func.isRequired
-        selected_file : rtypes.object   # if given, file selected by cursor, which we open on pressing enter
-        create_file_alert : rtypes.bool
-        files_displayed: rtypes.bool # True if there are any files displayed
+        file_search        : rtypes.string
+        current_path       : rtypes.string
+        actions            : rtypes.object.isRequired
+        create_file        : rtypes.func.isRequired
+        create_folder      : rtypes.func.isRequired
+        selected_file      : rtypes.object   # if given, file selected by cursor, which we open on pressing enter
+        file_creation_error: rtypes.string
+        files_displayed    : rtypes.bool # True if there are any files displayed

     getDefaultProps : ->
         file_search : ''
@@ -1381,11 +1385,14 @@
                 {text}
             </Alert>

-    render_create_file_warning : ->
-        if @props.create_file_alert
-            <Alert style={wordWrap:'break-word'} bsStyle='warning'>
-                You must enter file name above to create it
+    render_file_creation_error : ->
+        if @props.file_creation_error
+            <Alert style={wordWrap:'break-word'} bsStyle='warning' onDismiss=@dismiss_alert>
+                {@props.file_creation_error}
             </Alert>
+
+    dismiss_alert : ->
+        @props.actions.setState(file_creation_error : '')

     search_submit: ->
         # TODO: Add visual indication that a file is selected at all.
@@ -1412,7 +1419,7 @@
                 on_change     = {@props.actions.set_file_search}
                 on_submit     = {@search_submit}
             />
-            {@render_create_file_warning()}
+            {@render_file_creation_error()}
             {@render_help_info()}
         </span>

@@ -1441,9 +1448,9 @@
         </MenuItem>

     handle_menu_click : (ext) ->
-        if @props.file_search.length == 0
+        if not @props.file_search?.length > 0
             # Tell state to render an error in file search
-            @props.actions.setState(create_file_alert : true)
+            @props.actions.setState(file_creation_error : "You must enter file name above to create it")
         else if @props.file_search?[@props.file_search.length - 1] == '/'
             @props.create_folder()
         else
@@ -1451,7 +1458,7 @@

     # Go to new file tab if no file is specified
     handle_file_click : ->
-        if @props.file_search.length == 0
+        if not @props.file_search?.length > 0
             @props.actions.set_focused_page('project-new-file')
         else if @props.file_search?[@props.file_search.length - 1] == '/'
             @props.create_folder()
@@ -1488,16 +1495,16 @@
         account :
             other_settings : rtypes.immutable
         "#{name}" :
-            current_path      : rtypes.string
-            activity          : rtypes.object
-            page_number       : rtypes.number
-            file_action       : rtypes.string
-            file_search       : rtypes.string
-            show_hidden       : rtypes.bool
-            sort_by_time      : rtypes.bool
-            error             : rtypes.string
-            checked_files     : rtypes.immutable
-            create_file_alert : rtypes.bool
+            current_path       : rtypes.string
+            activity           : rtypes.object
+            page_number        : rtypes.number
+            file_action        : rtypes.string
+            file_search        : rtypes.string
+            show_hidden        : rtypes.bool
+            sort_by_time       : rtypes.bool
+            error              : rtypes.string
+            checked_files      : rtypes.immutable
+            file_creation_error : rtypes.string

     propTypes :
         project_id    : rtypes.string
@@ -1522,8 +1529,11 @@
             ext          : ext
             current_path : @props.current_path
             on_download  : ((a) => @setState(download: a))
-            on_error     : ((a) => @setState(error: a))
+            on_error     : @handle_creation_error
         @props.actions.setState(file_search : '', page_number: 0)
+
+    handle_creation_error : (e) ->
+        @props.actions.setState(file_creation_error : e)

     create_folder : ->
         @props.actions.create_folder(@props.file_search, @props.current_path, (a) => setState(error: a))
@@ -1724,15 +1734,15 @@
             <Row>
                 <Col sm=3>
                     <ProjectFilesSearch
-                        key               = {@props.current_path}
-                        file_search       = {@props.file_search}
-                        actions           = {@props.actions}
-                        current_path      = {@props.current_path}
-                        selected_file     = {visible_listing?[0]}
-                        create_file_alert = {@props.create_file_alert}
-                        files_displayed   = {visible_listing?.length > 0}
-                        create_file       = {@create_file}
-                        create_folder     = {@create_folder} />
+                        key                = {@props.current_path}
+                        file_search        = {@props.file_search}
+                        actions            = {@props.actions}
+                        current_path       = {@props.current_path}
+                        selected_file      = {visible_listing?[0]}
+                        file_creation_error = {@props.file_creation_error}
+                        files_displayed    = {visible_listing?.length > 0}
+                        create_file        = {@create_file}
+                        create_folder      = {@create_folder} />
                 </Col>
                 {@render_new_file() if not public_view}
                 <Col sm={if public_view then 6 else 4}>


Ensure there's a block before testing it. Fixes #310

--- src/trix/models/composition.coffee
+++ src/trix/models/composition.coffee
@@ -202,7 +202,7 @@
         true

   canSetCurrentBlockAttribute: (attributeName) ->
-    block = @getBlock()
+    return unless block = @getBlock()
     not block.isTerminalBlock()

   setCurrentAttribute: (attributeName, value) ->


Switch back to the @mention as primary name

Improves help display, etc

--- src/quip.coffee
+++ src/quip.coffee
@@ -73,7 +73,8 @@
         @robot.logger.error "Giving up"
     else
       @socketUrl = response.url
-      @selfMention = @quipMention response.user_id
+      @robotName = @robot.name
+      @robot.name = @quipMention response.user_id
       @connect()

   quipMention: (userId) ->
@@ -130,12 +131,12 @@
       when "error"
         @robot.logger.error packet.debug
       when "message"
-        if -1 != @selfMention.indexOf packet.user.id
+        if -1 != @robot.name.indexOf packet.user.id
           return
         user = @robot.brain.userForId packet.user.id, name: @quipMention packet.user.id, room: packet.thread.id
         text = packet.message.text
-        if @selfMention == text.substr 0, @selfMention.length
-          text = @robot.name + text.substr @selfMention.length
+        if @robotName == text.substr 0, @robotName.length
+          text = @robot.name + text.substr @robotName.length
         message = new QuipTextMessage user, text, packet
         @robot.receive message
       else


add teams

add teams

--- src/grammar/object-matcher.coffee
+++ src/grammar/object-matcher.coffee
@@ -22,6 +22,10 @@
     /orgs/ [^/]+
     $
   ///
+  'teams': /// ^ (https?://[^/]+)? (/api/v3)?
+    /teams/ [^/]+
+    $
+  ///
   'repos.comments': /// ^
     (https?://[^/]+)? # Optional protocol, host, and port
     (/api/v3)?        # Optional API root for enterprise GitHub users


Spelling

Spelling

--- src/repo.coffee
+++ src/repo.coffee
@@ -2,7 +2,7 @@

 class App.Repo
-  # @param repo_spec shoudl be a string like "facebook/react"
+  # @param repo_spec should be a string like "facebook/react"
   constructor: (repo_spec) ->
     parts = repo_spec.split('/')
     @acct = parts[0]


Re-fix image search not working

Re-fix image search not working

--- plugins/image.coffee
+++ plugins/image.coffee
@@ -7,7 +7,7 @@
             editable: null
             toolbar: null
             uuid: ""
-            searchUrl: "app_dev.php/liip/vie/assets/search" #TODO: pass this in, set tdefault to ""
+            searchUrl: "liip/vie/assets/search" #TODO: pass this in, set tdefault to ""
             dialogOpts:
                 autoOpen: false
                 width: 270
@@ -56,7 +56,7 @@

-                    <form action=\"#{@options.searchUrl}/?page=1&length=4\" tpye=\"post\" id=\"search_form\">
+                    <form action=\"#{widget.options.searchUrl}/?page=1&length=4\" tpye=\"post\" id=\"search_form\">
                         <input type=\"text\" class=\"searchInput\" /><input type=\"submit\" class=\"searchButton\" value=\"OK\"/>
                     </form>

@@ -73,7 +73,7 @@
                 that = @
                 jQuery.ajax({
                     type: "GET",
-                    url: @options.searchUrl,
+                    url: widget.options.searchUrl,
                     data: "page=1&length=4&searchString=2",
                     success: (response) ->
                         items = Array()


fixed logic error with return value from up-function

--- src/runr.coffee
+++ src/runr.coffee
@@ -14,15 +14,13 @@
 setNow = if typeof setImmediate != 'undefined' then setImmediate else (callback) -> setTimeout(callback, 0)

 exports.up = (name, args, callback) ->
+  f = null
+  result = (cb) -> if f then f(cb) else cb()
+
   exports[name].isRunning (err, isRunning) ->
     # We always want to return a function, so the interface remains uniform
     # If "up" started the process, then calling the function kills if.
     # If "up" did not start the process, then calling the function does nothing.
-    if err?
-      setNow(-> callback(err))
-      return ->
-    if isRunning
-      setNow(callback)
-      return ->
-    exports[name].run(args, callback)
-  +    return setNow(-> callback(err)) if err?
+    return setNow(callback) if isRunning
+    f = exports[name].run(args, callback)


fixed textwrap issue w/ 2nd line truncation

--- src/textwrap/helpers/tspan.coffee
+++ src/textwrap/helpers/tspan.coffee
@@ -58,12 +58,12 @@

   truncate = ->
     textBox.remove()
-    line--
-    if line isnt 1
-      if reverse
-        textBox = vars.container.value.select("tspan")
-      else
-        textBox = d3.select(vars.container.value.node().lastChild)
+    if reverse
+      line++
+      textBox = vars.container.value.select("tspan")
+    else
+      line--
+      textBox = d3.select(vars.container.value.node().lastChild)
     unless textBox.empty()
       words = textBox.text().match(/[^\s-]+-?/g)
       ellipsis()
@@ -85,7 +85,7 @@
       else
         if vars.text.split.value.indexOf(lastChar) >= 0
           lastWord = lastWord.substr(0, lastWord.length - 1)
-        textBox.text words.join(" ") + " " + lastWord + " ..."
+        textBox.text words.join(" ") + " " + lastWord + "..."
         ellipsis() if textBox.node().getComputedTextLength() > lineWidth()
     else
       truncate()
@@ -137,6 +137,9 @@
         unsafe = vars.text.split.value.indexOf(next_char) >= 0
         placeWord next_char if unsafe

+    if line * dy > height
+      truncate()
+
     lines = Math.abs(line - start) + 1

   wrap()


Partial fix for #390. Show error output when push command fails

--- lib/views/remote-list-view.coffee
+++ lib/views/remote-list-view.coffee
@@ -84,3 +84,6 @@
       .then (message) ->
         view.addLine(message).finish()
         startMessage.dismiss()
+      .catch (error) ->
+        view.addLine(error).finish()
+        startMessage.dismiss()


added support for cpp, cxx extensions and their header files (h, hpp, hxx), fixed typo


Fix searchWithPrefix

Fix searchWithPrefix

--- src/trie.coffee
+++ src/trie.coffee
@@ -295,7 +295,7 @@
     root = @find_ @root_, codepoints
     return if !root?
     callback prefix, root if root.v?
-    @traverse_ root.m, (key, node) -> callback prefix + key, node.v
+    @traverse_ root.m, (key, value) -> callback prefix + key, value
     return this

   keysWithCommonPrefix: (key) ->
@@ -389,13 +389,13 @@
             ps.push prefix
             prefix += fromCodePoint root.c
             if root.v?
+              result.done = false
               if callback?
                 result.value = {key: prefix, node: root}
                 result = callback result
               else
                 result.value = {key: prefix, value: root.v}
               root = root.m
-              result.done = false
               return result
             else
               root = root.m


fix internal / public api register

fix internal / public api register

--- src/util.coffee
+++ src/util.coffee
@@ -8,8 +8,10 @@

   for own name, proc of reference
     isInternal = name.match /^_/i
-    internal[if isInternal then name.substring 1 else name] = proc
-    @[name] = proc unless isInternal
+    if isInternal
+      internal[name.substring 1] = proc
+    else
+      @[name] = proc
   @

 pp.extend (util) ->


cache media:thumbnail files

cache media:thumbnail files

--- cache_builder/cb.coffee
+++ cache_builder/cb.coffee
@@ -110,22 +110,31 @@
             add_shorturl shorturls,url+'&a='+encodeURIComponent(appurls[0])

+add_fileurl = (url, fileurls) ->
+    #console.log "check #{url}"
+    # not sure why indexOf doesn't seem to match it
+    us = for u in fileurls when u == url
+      u
+    if not us? or us.length == 0
+      #console.log 'add '+url+' to fileurls'
+      fileurls.push url
+
 # cache entry for each
 make_cache = (feed,cache) ->

   fileurls = []
   for entry in feed.entry
+    #console.dir entry
     hidden = is_hidden entry
+    # atom enclosures
     for link in entry.link when link.$.href? and (link.$.rel == 'alternate' or not hidden)
-      url = link.$.href
-      # not sure why indexOf doesn't seem to match it
-      us = for u in fileurls when u == url
-        u
-      if us.length == 0
-        #console.log 'add '+url+' to '+fileurls
-        fileurls.push url
-  #console.dir fileurls
+      add_fileurl link.$.href, fileurls
+    # media-rss media:thumbnail
+    for thumbnail in (entry['media:thumbnail'] ? []) when thumbnail.$.url?
+      add_fileurl thumbnail.$.url, fileurls
+  console.dir fileurls

+
   oldfiles = cache.files ? []
   cache.files = []


Attempt to reduce the code pyramid

Attempt to reduce the code pyramid

--- library/membrane/bilateral.coffee
+++ library/membrane/bilateral.coffee
@@ -108,6 +108,12 @@
         assert _.isFunction i = Marshal.deserialize
         uplinking = "Uplink %s at #{@location}, nsp=%s"
         ack = "Ack an uplink %s at #{@location}, nsp=%s"
+        responder = (socket, mangled, callback) -> ->
+            return c(o(arguments)...) if c = callback
+            id = socket.sacks = (socket.sacks ?= 0) + 1
+            packet = type: "ack", name: mangled, ack: "data"
+            _.extend packet, ackId: id, args: o(arguments)
+            assert packet.ackId; socket.packet packet
         _.forIn this, (value, reference, context) =>
             return unless reference of (@uplinks or {})
             assert mangled = "#{@location}/#{reference}"
@@ -115,13 +121,9 @@
             @socket.on mangled, (args..., callback) =>
                 args.push c unless _.isFunction c = callback
                 callback = null unless _.isFunction callback
+                reply = responder @socket, mangled, callback
                 console.log uplinking, reference, nsp
-                assert args; return value i(args)..., =>
-                    return c(o(arguments)...) if c = callback
-                    id = @socket.sacks = (@socket.sacks ?= 0) + 1
-                    packet = type: "ack", name: mangled, ack: "data"
-                    _.extend packet, ackId: id, args: o(arguments)
-                    assert packet.ackId; @socket.packet packet
+                return value i(args)..., reply

     # This is a complementary part of the bilateral implementation.
     # It is invoked to produce a server side agent that is aware of
@@ -136,14 +138,17 @@
         notify = "incorrect callback for the uplink"
         assert _.isFunction o = Marshal.serialize
         assert _.isFunction i = Marshal.deserialize
+        uploader = (socket, callback, ptr) -> ->
+            predicate = (ack, key) -> ack is ptr()
+            key = _.findKey socket.acks, predicate
+            assert key, "ack"; delete socket.acks[key]
+            logger.debug responded.cyan, name.bold
+            return callback.apply this, i(arguments)
         return (parameters..., callback=(->)) =>
             assert _.isFunction(callback), notify
             assert mangled = "#{@location()}/#{name}"
             assert binder = socket.binder, noBinder
             mangled += "/#{nsp}" if nsp = binder.nsp
             logger.debug uplinking.cyan, name.bold
-            socket.emit mangled, o(parameters)..., a = =>
-                key = _.findKey socket.acks, (x) -> x is a
-                assert key, "ack"; delete socket.acks[key]
-                logger.debug responded.cyan, name.bold
-                return callback.apply this, i(arguments)
+            ack = uploader socket, callback, -> ack
+            socket.emit mangled, o(parameters)..., ack


add wss for csp

add wss for csp

--- config/http.coffee
+++ config/http.coffee
@@ -9,7 +9,7 @@
 				host = req.headers['x-forwarded-host'] || req.headers['host']
 				ret = csp
 					directives:
-						connectSrc: [ "'self'", "ws://#{host}" ]
+						connectSrc: [ "'self'", "ws://#{host}", "wss://#{host}" ]
 						styleSrc: [ "'self'", "'unsafe-inline'" ]
 						scriptSrc: [ "'self'", "'unsafe-inline'", "'unsafe-eval'" ]
 				ret req, res, next


Avoid race comditions in background completer, change id handling.

--- pages/vomnibar.coffee
+++ pages/vomnibar.coffee
@@ -219,10 +219,9 @@
     chrome.runtime.sendMessage({ handler: "refreshCompleter", name: @name })

   filter: (query, callback) ->
-    id = BackgroundCompleter.messageId += 1
     @filterPort.onMessage.addListener (msg) =>
       @filterPort.onMessage.removeListener(arguments.callee)
-      if id == BackgroundCompleter.messageId
+      if msg.id == BackgroundCompleter.messageId
         # The result objects coming from the background page will be of the form:
         #   { html: "", type: "", url: "" }
         # type will be one of [tab, bookmark, history, domain].
@@ -235,7 +234,7 @@
           result
         callback(results)

-    @filterPort.postMessage({ id: id, name: @name, query: query })
+    @filterPort.postMessage({ id: ++BackgroundCompleter.messageId, name: @name, query: query })

 extend BackgroundCompleter,
   #


add test for self dependency

add test for self dependency


Use featured image for social sharing thumbnail if there is one. Closes #192.

--- collections/post.coffee
+++ collections/post.coffee
@@ -35,10 +35,15 @@
     @body

   thumbnail: ->
-    regex = new RegExp /img src=[\'"]([^\'"]+)/ig
-
-    while match = regex.exec @body
-      return match[1]
+    if @featuredImage?
+      if Meteor.settings?.public?.blog?.useS3
+        @featuredImage
+      else
+        Meteor.absoluteUrl() + @featuredImage
+    else
+      regex = new RegExp /img src=[\'"]([^\'"]+)/ig
+      while match = regex.exec @body
+        return match[1]

   @excerpt: (html) ->
     if Blog.settings.excerptFunction?


Use isAtEndOfLine() method instead of homemade

--- lib/motions/general-motions.coffee
+++ lib/motions/general-motions.coffee
@@ -133,7 +133,7 @@

       return if @isEndOfFile()

-      if @isEndOfLine()
+      if cursor.isAtEndOfLine()
         cursor.moveDown()
         cursor.moveToBeginningOfLine()
         cursor.skipLeadingWhitespace()
@@ -157,11 +157,6 @@
         @editor.selectToBeginningOfNextWord()

       true
-
-  isEndOfLine: ->
-    cur = @editor.getCursor().getBufferPosition()
-    end = @editor.getCursor().getCurrentLineBufferRange().end
-    cur.row is end.row and cur.column is end.column

   isEndOfFile: ->
     cur = @editor.getCursor().getBufferPosition()


reorder condition for readability

reorder condition for readability

--- lib/motions/general-motions.coffee
+++ lib/motions/general-motions.coffee
@@ -150,7 +150,7 @@
   moveCursor: (cursor, count=1) ->
     _.times count, =>
       cursor.moveRight() unless cursor.isAtEndOfLine()
-      cursor.moveRight() if cursor.isAtEndOfLine() and atom.config.get('vim-mode.wrapLeftRightMotion')
+      cursor.moveRight() if atom.config.get('vim-mode.wrapLeftRightMotion') and cursor.isAtEndOfLine()
       @ensureCursorIsWithinLine(cursor)

 class MoveUp extends Motion


removed old ctrl.coffee template from repo

--- templates/ctrl.coffee
+++ templates/ctrl.coffee
@@ -1,17 +0,0 @@
-# Require your controller in routes.coffee!
-
-log = require('logule').init(module, 'CTRL_NAME CONTROLLER')
-fs  = require('fs')
-
-class CTRL_NAMEController
-  constructor: (app) ->
-    @app = app
-
-  serveFile: (req, res) ->
-    path = "#{__dirname}/../../client/layouts/default.html"
-
-    fs.readFile path, 'utf-8', (err, data) ->
-      res.set 'Content-Type', 'text/html'
-      res.send data
-
-module.exports = CTRL_NAMEController


updated protractor runner config

updated protractor runner config

--- test/e2e/protractor.conf.coffee
+++ test/e2e/protractor.conf.coffee
@@ -1,18 +1,19 @@
-# require 'jasmine-reporters'
-wrench = require 'wrench'
+onPrepare = () ->
+  require 'jasmine-reporters'
+  wrench = require 'wrench'

-onPrepare = () ->
-  # dir = 'test/report/protractor/'
+  dir = 'test/report/protractor/'

   # https://github.com/angular/protractor/issues/38#issuecomment-29801069
   global.select = global.by

-  # wrench.mkdirSyncRecursive dir
+  wrench.mkdirSyncRecursive dir

-  # # https://github.com/angular/protractor/issues/268
-  # jasmine.getEnv().addReporter new jasmine.JUnitXmlReporter dir, true, true
+  # https://github.com/angular/protractor/issues/268
+  jasmine.getEnv().addReporter new jasmine.JUnitXmlReporter dir, true, true

 config =
+  framework: 'jasmine'
   onPrepare: onPrepare
   specs: [
     '**/*-step.js'


Fix 145c665 screwing up floating-point numbers


Update - enable generate slug test.

Update - enable generate slug test.

--- test/product-import.spec.coffee
+++ test/product-import.spec.coffee
@@ -310,7 +310,7 @@

   describe '::_generateSlug', ->

-    xit 'should generate valid slug', ->
+    it 'should generate valid slug', ->
       sampleName =
         name:
           en: 'sample_product_name'


Skiping content type ajax test

Skiping content type ajax test

--- test/ajax.spec.coffee
+++ test/ajax.spec.coffee
@@ -42,7 +42,7 @@
             done()

     test 'content type', (done) ->
-        countdown = new Number.Countdown(3, done)
+        countdown = new Number.Countdown(2, done)
         ajax.request url: '/content', method: 'post', (data, settings) ->
             assert.equal data, 'content type application/x-www-form-urlencoded'
             countdown.fire()
@@ -51,9 +51,9 @@
             assert.equal data, 'content type undefined'
             countdown.fire()

-        ajax.request url: '/content', contentType: 'text/plain', (data, settings) ->
-            assert.equal data, 'content type text/plain'
-            countdown.fire()
+        # ajax.request url: '/content', contentType: 'text/html', (data, settings) ->
+        #     assert.equal data, 'content type text/html'
+        #     countdown.fire()

     test 'requested with', (done) ->
         countdown = new Number.Countdown(3, done)


collision with regular bounds

collision with regular bounds

--- test/collision.coffee
+++ test/collision.coffee
@@ -57,5 +57,5 @@
   start = Point(0, 0)
   direction = Point(1, 0)

-  Collision.rayRectangle start,
+  Collision.rayRectangle start, rect.bounds
 module()


uncommenting tests for contracts that run in node

--- test/contracts.coffee
+++ test/contracts.coffee
@@ -1,55 +1,66 @@
-# note that these tests are really just checking
-# the grammar. Can't actually test that the contracts are working
-# beause we depend on Proxy which node (aka V8) doesn't have yet.
-# Actual contract functionality tests are found in webtests/.
+test "function, simple first order", ->
+  id :: (Num) -> Num
+  id = (x) -> x
+  id = id.use()

-# test "function, simple first order", ->
-#   id :: (Num) -> Num
-#   id = (x) -> x
-#   eq (id 4), 4
+  eq (id 4), 4

-# test "function, higher order in params", ->
-#   id :: ( ((Num) -> Str), Bool ) -> Num
-#   id = (x) -> x
+test "function violates contract", ->
+	id :: (Num) -> Num
+	id = (x) -> x
+	id = id.use()

-#   eq (id 4), 4
+	throws (-> id "foo"), Error

-# # test "function, higher order in params, implicit parens", ->
-# #   id :: ( (Num) -> Str, Bool ) -> Num
-# #   id = (x) -> x
+test "function, higher order in params", ->
+  id :: ( ((Num) -> Str), Bool ) -> Num
+  id = (x) -> x
+  id = id.use()

-# #   eq (id 4), 4
+  throws (-> id 4), null

-# test "function, higher order in range", ->
-#   id :: (Num) -> (Str) -> Bool
-#   id = (x) -> x
-#   eq (id 4), 4
+test "function, higher order in params, implicit parens", ->
+  id :: ( (Num) -> Str, Bool ) -> Num
+  id = (x) -> x
+  id = id.use()

-# test "object, simple props", ->
-#   id :: { a: Num, b: Str }
-#   id =
-#     a: 42
-#     b: "foo"
-#   eq id.a, 42
+  throws (-> id 4), null

-# test "object, function props", ->
-#   id :: { a: ( (Num) -> Num ), b: Str }
-#   id =
-#     a: (x) -> x
-#     b: "foo"
-#   eq (id.a 42), 42
+test "function, higher order in range", ->
+  id :: (Num) -> (Str) -> Bool
+  id = (x) -> x
+  id = id.use()
+
+  throws (-> id 4), null
+
+test "object, simple props", ->
+  id :: { a: Num, b: Str }
+  id =
+    a: 42
+    b: "foo"
+  id = id.use()
+  eq id.a, 42
+
+test "object, function props", ->
+  id :: { a: ( (Num) -> Num ), b: Str }
+  id =
+    a: (x) -> x
+    b: "foo"
+  id = id.use()
+
+  eq (id.a 42), 42

 # test "array, simple props", ->
 #   id :: [Num, Str]
 #   id = [42, "foo"]
+#   id = id.use()
+
 #   eq id[0], 42

 # test "array, function props", ->
 #   id :: [ ((Num) -> Num), Str]
-#   id = [(x) -> x, "foo"]
+#   id = [((x) -> x), "foo"]
+#   id = id.use()
+
 #   eq (id[0] 42), 42

-# todo make a test that catches
-# a :: C
-# b = C
-# idents need to match up


fixed test

fixed test

--- test/api.coffee
+++ test/api.coffee
@@ -28,6 +28,12 @@
     checkResult result

 describe 'model', ->
+  afterEach (done) ->
+    if Factory.connected
+      User.remove(done)
+    else
+      done()
+
   it 'should save data', ->
     should.exist User, 'expected User model to be defined'
     User.create Factory.userData, (err, user) ->


Fix memRequested notification

Fix memRequested notification

--- angular/core/components/notification/notification.coffee
+++ angular/core/components/notification/notification.coffee
@@ -5,14 +5,12 @@
   replace: true
   controller: ($scope, Records) ->
     $scope.actor = ->
-      $scope.notification.actor() or $scope.membershipActor()
+      $scope.membershipRequestActor || $scope.notification.actor()

-    $scope.membershipActor = ->
-      return unless $scope.notification.kind == 'membership_requested'
-      name = $scope.notification.translationValues.name
-      Records.users.build
-        name:           name
-        avatarInitials: name.toString().split(' ').map((n) -> n[0]).join('')
+    if $scope.notification.kind == 'membership_requested'
+      $scope.membershipRequestActor = Records.users.build
+        name:           $scope.notification.translationValues.name
+        avatarInitials: $scope.notification.translationValues.name.toString().split(' ').map((n) -> n[0]).join('')
         avatarKind:     'initials'

     return


Bugfix: Missing brackets causes calendar_subscriptions layout_ref to not render properly.

--- app/assets/javascripts/app/controllers/layout_ref.coffee
+++ app/assets/javascripts/app/controllers/layout_ref.coffee
@@ -1556,7 +1556,7 @@
     @render()

   render: ->
-    @html App.view('layout_ref/calendar_subscriptions')
+    @html App.view('layout_ref/calendar_subscriptions')()

   selectAll: (e) ->
     e.currentTarget.focus()


Re-enable client db

Re-enable client db

--- app/assets/javascripts/application/seeds_store.js.coffee
+++ app/assets/javascripts/application/seeds_store.js.coffee
@@ -4,7 +4,7 @@
   constructor: (name='seeds') ->
     @initDb(name)
   initDb: (name) ->
-    podsSchema =
+    podsSchema =
       name: 'pod'
       keyPath: 'name'
       type: 'TEXT'
@@ -22,7 +22,7 @@
       indexes: []
     @db = new ydn.db.Storage name, stores: [podsSchema, categoriesSchema]
   clear: () ->
-    @db.clear()
+    @db.clear()
   # this reads from _all_ pods and uses ydn internal capabilities for sorting
   # and pagination
   findPods: (sortBy, asc=true, offset=0, limit=50) ->
@@ -57,7 +57,7 @@
   countPods: () ->
     @db.count('pod')
   updatePods: (pods) ->
-    #@db.put('pod', pods)
+    @db.put('pod', pods)
   findCategories: () ->
     iterator = new ydn.db.ValueIterator("category")
     @db.values iterator, 1000
@@ -65,5 +65,5 @@
     keyRange = ydn.db.KeyRange.only(name)
     @db.values 'category', keyRange
   updateCategories: (categories) ->
-    #@db.clear('category')
-    #@db.put('category', categories)
+    @db.clear('category')
+    @db.put('category', categories)


Remove title count column from player table

--- app/assets/javascripts/components/player-table.js.coffee
+++ app/assets/javascripts/components/player-table.js.coffee
@@ -6,7 +6,6 @@
   "Player"
   "Matchup Winning %"
   "Matchup Count"
-  "Titles Won"
 ]

 @PlayerTable = React.createClass
@@ -68,8 +67,3 @@
 	            scope: "row"
 	            className: "league--table-cell"
 	            player.matchup_count
-
-	          td
-	            scope: "row"
-	            className: "league--table-cell"
-	            # player.titles


Improve clarity of calendar panel helper text.

The term 'blackout dates' is not necessarily clear.

--- app/assets/javascripts/components/wizard/form_panel.cjsx
+++ app/assets/javascripts/components/wizard/form_panel.cjsx
@@ -88,7 +88,7 @@
     <Panel {...@props}
       raw_options={raw_options}
       nextEnabled={@nextEnabled}
-      helperText = 'Choose blackout dates to continue'
+      helperText = 'Select meeting days and holiday dates, then continue.'
     />
 )


Tweak slide animation

Tweak slide animation

--- app/assets/javascripts/controllers/submissions.js.coffee
+++ app/assets/javascripts/controllers/submissions.js.coffee
@@ -33,7 +33,7 @@
   # fetch_tree_for_path(gon.tree_root_path)
   $("#scroll_to_uploads").on("click", (event) ->
     event.preventDefault()
-    $.scrollTo("#fileupload", 1500, easing: "easeOutElastic")
+    $.scrollTo("#fileupload", 500, easing: "easeInOutSine")
     )


Still working on it...

Still working on it...

--- app/assets/javascripts/sprangular/services/status.coffee
+++ app/assets/javascripts/sprangular/services/status.coffee
@@ -14,7 +14,7 @@
       @httpLoading || @routeChanging

     cacheProduct: (product) ->
-      status.cacheProducts.push product
+      status.cachedProducts.push(product)

     cacheProducts: (list) ->
       status.cachedProducts = status.cachedProducts.concat(list)


fix canceling the reschedule dialog

fix canceling the reschedule dialog

test plan:
 - enable new scheduler
 - have an appointment group that allows students to sign up
   for only one time slot
 - as a student, enter Find Appointment Mode and sign up for a time slot
 - attempt to sign up for another slot
 - when the "Cancel and reschedule" dialog appears, click
   "Do Nothing" or the X icon in the corner
 - you should still be in find-appointment mode, and the slot
   you failed to sign up for:
   (1) should not be grayed out
   (2) should respond to clicks

to test the non-handled error case:
 - have an appointment group that allows only one sign-up per slot
 - put two separate students (in two different browsers) in find-
   appointment mode
 - sign up for a slot as one student, and without refreshing the
   other student's page, try to sign up for the same slot
 - the second student should see a non-development "Oops!" error
   with a details link (and _not_ an alert full of JSON)

fixes CNVS-33654

Change-Id: Iacf894800e181599f21585054b30a89e136db10e
Reviewed-on: https://gerrit.instructure.com/96669
Tested-by: Jenkins
Reviewed-by: Felix Milea-Ciobanu <9384aa226ac9d3237ed0bc52e18ffcb251ee0bf7@instructure.com>
QA-Review: Deepeeca Soundarrajan <5bcd4b623f00b7fc2942cd1c65c245b30a48a61f@instructure.com>
Product-Review: Jeremy Stanley <b3f594e10a9edcf5413cf1190121d45078c62290@instructure.com>

--- app/coffeescripts/calendar/ShowEventDetailsDialog.coffee
+++ app/coffeescripts/calendar/ShowEventDetailsDialog.coffee
@@ -49,7 +49,8 @@
         success: () =>
           $.publish "CommonEvent/eventDeleted", event

-    reserveErrorCB: (data) =>
+    reserveErrorCB: (data, request) =>
+      $.publish "CommonEvent/eventSaveFailed", @event
       for error in data when error.message is 'participant has met per-participant limit'
         errorHandled = true
         error.reschedulable = error.reservations.length == 1
@@ -72,10 +73,10 @@
                        text: I18n.t 'ok', 'OK'
                        click: -> $dialog.dialog('close')
                      ]
-
       unless errorHandled
-        alert "Could not reserve event: #{data}"
-        $.publish "CommonEvent/eventSaveFailed", @event
+        # defer to the default error dialog
+        $.ajaxJSON.unhandledXHRs.push(request);
+        $.fn.defaultAjaxError.func.apply($.fn.defaultAjaxError.object, arguments)

     reserveSuccessCB: (cancel_existing, data) ->
       @popover.hide()


Update description of "Poet Mode"

Update description of "Poet Mode"

--- src/PreferPoetMode.coffee
+++ src/PreferPoetMode.coffee
@@ -25,7 +25,8 @@
       </code>
       </pre>
       This rule enforces that implicit parens be used whenever
-      possible since their use is idiomatic CoffeeScript.
+      a line ends with a function invocation with one or more
+      arguments.
       '''
   constructor: ->


Always trigger changePage when new content is fetched

--- src/scripts/models/content.coffee
+++ src/scripts/models/content.coffee
@@ -91,8 +91,9 @@
         @setPage(page or 1) # Default to page 1
       else
         @set('currentPage', new Page({id: @id}))
-        @get('currentPage').fetch()
-        @trigger('changePage')
+        @get('currentPage').fetch
+          success: () =>
+            @trigger('changePage')

     setPage: (num) ->
       if num < 1 then num = 1


Mark pages and books as editable on load if they are drafts

--- src/scripts/models/content.coffee
+++ src/scripts/models/content.coffee
@@ -46,10 +46,9 @@
     parse: (response, options = {}) ->
       super(arguments...)

-      if response.mediaType is 'application/vnd.org.cnx.collection'
-        # Mark drafts as being in edit mode by default
-        if @isDraft() and response.status isnt 'publishing'
-          response.editable = true
+      # Mark drafts as being in edit mode by default
+      if @isDraft() and response.status isnt 'publishing'
+        response.editable = true

       return response


Revert "webapp/files: details around links + help for sharing files"

This reverts commit ba9c93f450b6b7709c9819421a2af7595ab373aa.

--- src/smc-webapp/project_files.cjsx
+++ src/smc-webapp/project_files.cjsx
@@ -1072,7 +1072,7 @@

-WIKI_SHARE_HELP_URL = 'https://doc.cocalc.com/share.html'
+WIKI_SHARE_HELP_URL = 'https://github.com/sagemathinc/cocalc/wiki/share'

 ProjectFilesActionBox = rclass
     displayName : 'ProjectFiles-ProjectFilesActionBox'
@@ -1557,7 +1557,7 @@

     render_share_defn: ->

-            Use sharing to make a file or directory visible to the world.   (If you would instead like to privately collaborate and chat with people in this project, go the Project Settings tab and "Add people to project".)
+            Use sharing to make a file or directory visible to the world.   (If you would instead like to privately collaborate and chat with people in this project, go the project Settings tab and "Add people to project".)

     set_public_file_unlisting_to: (new_value) ->
@@ -1702,10 +1702,10 @@
             <FormGroup>
             {if @props.get_total_project_quotas(@props.project_id)?.network then <Radio name="sharing_options" value="public_listed" checked={state == "public_listed"} onChange={handler} inline>
                     <Icon name='eye'/><Space/>
-                    Public (listed) - This will appear on the public share server.
+                    Public (listed) - This will appear on the public share server.
               </Radio> else <Radio disabled={true} name="sharing_options" value="public_listed" checked={state == "public_listed"} inline>
                     <Icon name='eye'/><Space/>
-                    Public (listed) - This will appear on the share server. Public (listed) is only available for projects with network enabled.
+                    Public (listed) - This will appear on the share server. Public (listed) is only available for projects with network enabled.
                 </Radio>}

               <Radio name="sharing_options" value="public_unlisted" checked={state == "public_unlisted"} onChange={handler} inline>


created conditionals for managing different chat room menus

--- src/chat_room_view.coffee
+++ src/chat_room_view.coffee
@@ -45,25 +45,28 @@
     if @convo.message_filter is message.convo_id
       new_message = new Message(message.recipients, message.sender, message.content, message.image, message.time_stamp)
       @convo.add_message(new_message)
+      if @menu is "chat"
+        @renderDiscussion()
+        @$element.find('.top-bar').addClass('new-message')
+        @titleAlert()
+
+  user_offline_callback: ->
+    if @menu is "chat"
+      message = new Message( app.me, {image_url:'http://storage.googleapis.com/parley-assets/server_network.png'}, "This user is no longer online", false, new Date() )
+      @convo.add_message(message)
       @renderDiscussion()
-      @$element.find('.top-bar').addClass('new-message')
-      @titleAlert()
-
-  user_offline_callback: ->
-    message = new Message( app.me, {image_url:'http://storage.googleapis.com/parley-assets/server_network.png'}, "This user is no longer online", false, new Date() )
-    @convo.add_message(message)
-    @renderDiscussion()

   typing_notification_callback: (convo_id, typist, bool) ->
-    if convo_id is @convo.message_filter
-      if bool
-        if @$discussion.find('.incoming').length is 0
-          typing_notification = "

#{typist.display_name} is typing...

"
-          @$discussion.append(typing_notification)
+    if @menu is "chat"
+      if convo_id is @convo.message_filter
+        if bool
+          if @$discussion.find('.incoming').length is 0
+            typing_notification = "

#{typist.display_name} is typing...

"
+            @$discussion.append(typing_notification)
+            @scrollToLastMessage()
+        else
+          @$discussion.find('.incoming').remove()
           @scrollToLastMessage()
-      else
-        @$discussion.find('.incoming').remove()
-        @scrollToLastMessage()

   switch_to_persistent_convo: (e) ->
     e.preventDefault()


Added test for #23

Added test for #23

--- test/_security/A1-Injection.coffee
+++ test/_security/A1-Injection.coffee
@@ -36,7 +36,7 @@
       target_Folder.folder_Delete_Recursive().assert_Is_True()             # Delete temp folder

   # https://github.com/DinisCruz/BSIMM-Graphs/issues/20
-  it.only 'Issue 20 - Data_Files.set_File_Data - DoS via filename and file_Contents', ->
+  it 'Issue 20 - Data_Files.set_File_Data - DoS via filename and file_Contents', ->
     #@.timeout 5000
     # Create files with large strings
     using new Data_Files(), ->
@@ -52,7 +52,6 @@
         if should_Work                                        # if it should work
           file_Path.assert_File_Exists()                      #   confirm file exists
           file_Path.file_Delete().assert_Is_True()            #   delete temp file
-          file_Path.assert_File_Not_Exists()
         else                                                  # if not
           file_Path.assert_File_Not_Exists()                  #   confirm creation failed

@@ -72,3 +71,26 @@
       create_File 10 ,1000000 , true                           # 1 Mb
       #create_File 10 ,10000000 , true                          # 10 Mb - will work and take about 250 ms
       #create_File 10 ,100000000 , true                         # 100 Mb - will work and take about 2 secs
+
+
+  it 'Issue 23 - Data_Files.set_File_Data - allows creation of files with any extension', ->
+    using new Data_Files(), ->
+      create_File = (extension)=>
+        file_Name     = 10.random_String() + extension
+        file_Contents = 10.random_String()
+        file_Path     = @.data_Path.path_Combine(file_Name)
+
+        @.set_File_Data file_Name, file_Contents               # PAYLOAD: create file
+
+        file_Path.assert_File_Exists()                         #   confirm file exists
+                 .file_Delete().assert_Is_True()               #   delete temp file
+
+      create_File '.json'                                      # these are the ones that should work
+
+      create_File '.json5'                                     # these are the ones that should NOT work
+      create_File '.coffee'
+      create_File '.js'
+      create_File '.exe'
+      create_File '.html'
+      create_File '.css'
+      create_File '...'


add /test to watched directories

add /test to watched directories

--- src/commands/watch.coffee
+++ src/commands/watch.coffee
@@ -67,7 +67,7 @@

   initWatcher: (callback) ->
     watched = [
-      @config.paths.app, @config.paths.vendor,
+      @config.paths.app, @config.paths.vendor, @config.paths.test,
       @config.paths.config, @config.paths.packageConfig
     ].concat(@config.paths.assets)


Added z-index checking to specs of PageAnimation

--- spec/core/page_animation_spec.coffee
+++ spec/core/page_animation_spec.coffee
@@ -2,10 +2,21 @@
   beforeEach ->
     @pageEle1 = window.document.createElement('section')
     @pageEle2 = window.document.createElement('section')
+    @pageEle1.style.position = 'absolute'
+    @pageEle2.style.position = 'absolute'
+    window.document.body.appendChild(@pageEle1)
+    window.document.body.appendChild(@pageEle2)
+
     @prevPage = new Scena.Page(@pageEle1)
     @nextPage = new Scena.Page(@pageEle2)

   afterEach ->
+    window.document.body.removeChild(@pageEle1)
+    window.document.body.removeChild(@pageEle2)
+    @prevPage = null
+    @nextPage = null
+    @pageEle1 = null
+    @pageEle2 = null

   it 'constructs and destructs a object', ->
     pageAnimation = new Scena.PageAnimation(@prevPage, @nextPage, 'TestPageAnimation')
@@ -22,6 +33,8 @@

     expect(@pageEle1.classList.contains('prev')).toBeTruthy()
     expect(@pageEle2.classList.contains('next')).toBeTruthy()
+    expect(window.getComputedStyle(@pageEle1).zIndex) \
+      .toBeLessThan(window.getComputedStyle(@pageEle2).zIndex)

     pageAnimation.finalize()


removing physics to move it into extras

removing physics to move it into extras

--- src/engine_physics.coffee
+++ src/engine_physics.coffee
@@ -1,87 +0,0 @@
-( ($)->
-  {b2World, b2DebugDraw} = Box2D.Dynamics
-
-  ###*
-  (Module) The Box2D module provides physics integration via Box2D.
-
-  @name Box2D
-  @fieldOf Engine
-
-  @param {Object} I Instance variables
-  @param {Object} self Reference to the engine
-  ###
-  Engine.Box2D = (I, self) ->
-    $.reverseMerge I,
-      scale: 0.1
-      gravity: Point(0, 98)
-      PHYSICS_DEBUG_DRAW: false
-
-    world = new b2World(new Box2D.Common.Math.b2Vec2(I.gravity.x, I.gravity.y), true)
-    debugDraw = null
-    debugElement = null
-    debugCanvas = null
-
-    pendingCollisions = []
-    pendingDestructions = []
-
-    world.SetContactListener
-      BeginContact: (contact) ->
-        a = contact.GetFixtureA().GetBody().GetUserData()
-        b = contact.GetFixtureB().GetBody().GetUserData()
-
-        pendingCollisions.push([a, b, contact])
-
-      EndContact: (contact) ->
-      PreSolve: (contact, oldManifold) ->
-      PostSolve: (contact, impulse) ->
-
-    fireCollisionEvents = () ->
-      pendingCollisions.each (event) ->
-        [a, b, contact] = event
-
-        a.trigger "collision", b, contact
-        b.trigger "collision", a, contact
-
-      pendingCollisions = []
-
-    destroyPhysicsBodies = () ->
-      pendingDestructions.each (body) ->
-        world.DestroyBody(body)
-
-      pendingDestructions = []
-
-    self.bind "update", ->
-      world.Step(1 / I.FPS, 10, 10)
-      world.ClearForces()
-
-      fireCollisionEvents()
-      destroyPhysicsBodies()
-
-    self.bind "draw", (canvas) ->
-      if I.PHYSICS_DEBUG_DRAW
-        unless debugDraw
-          debugElement = $("<canvas width=640 height=480 />").get(0)
-          debugCanvas = debugElement.getContext("2d")
-          debugDraw = new b2DebugDraw()
-          debugDraw.SetSprite(debugCanvas)
-          debugDraw.SetDrawScale(10)
-          debugDraw.SetFillAlpha(0.3)
-          debugDraw.SetLineThickness(1.0)
-          debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit)
-          world.SetDebugDraw(debugDraw)
-
-        world.DrawDebugData()
-        canvas.withTransform I.cameraTransform, (canvas) ->
-          canvas.drawImage(debugElement, 0, 0, debugElement.width, debugElement.height, 0, 0, debugElement.width, debugElement.height)
-
-    self.bind "beforeAdd", (entityData) ->
-      entityData.world = world
-
-    self.bind "afterAdd", (object) ->
-      object.bind "remove", ->
-        pendingDestructions.push object.body()
-
-    return {}
-
-)(jQuery)
-


Activity: minor toggle quotes to single

Activity: minor toggle quotes to single

--- client/Social/Activity/ListItems/activitylistitem.coffee
+++ client/Social/Activity/ListItems/activitylistitem.coffee
@@ -2,8 +2,8 @@

   constructor:(options = {},data)->

-    options.type              = "activity"
-    options.cssClass          = "activity-item status"
+    options.type              = 'activity'
+    options.cssClass          = 'activity-item status'
     options.commentSettings or= {}

     super options, data
@@ -18,19 +18,19 @@
       size       :
         width    : 42
         height   : 42
-      cssClass   : "author-avatar"
+      cssClass   : 'author-avatar'
       origin     : origin

     @author     = new ProfileLinkView { origin }
     @commentBox = new CommentView options.commentSettings, data

     @actionLinks = new ActivityActionsView
-      cssClass : "comment-header"
+      cssClass : 'comment-header'
       delegate : @commentBox.commentList
     , data

     @settingsButton = new ActivitySettingsView
-      cssClass : "settings-menu-wrapper"
+      cssClass : 'settings-menu-wrapper'
       itemView : this
     , data

@@ -49,7 +49,7 @@
       delegate    : this

     @embedBox = if data.link?
-      @setClass "two-columns"  if @twoColumns
+      @setClass 'two-columns'  if @twoColumns
       new EmbedBox embedOptions, data.link
     else
       new KDCustomHTMLView
@@ -57,7 +57,7 @@
     @timeAgoView = new KDTimeAgoView {}, @getData().meta.createdAt

     @editWidgetWrapper = new KDCustomHTMLView
-      cssClass         : "edit-widget-wrapper hidden"
+      cssClass         : 'edit-widget-wrapper hidden'

   showEditWidget : ->


Fixing video modal when em mode is on

Fixing video modal when em mode is on

--- app/assets/javascripts/em.js.coffee
+++ app/assets/javascripts/em.js.coffee
@@ -45,22 +45,32 @@
         showMobileTab mobileSections

     $('.em-preview-modal').hide()
-    $.get window.location.origin + "/em", (response) ->
-        if !response.em_mode
-            if $(window).width() > 480 and $(window).height() > 480
-                $("#joyride-steps").joyride()
-                #strange bug, if called once, it doesn't work
-                $("#joyride-steps").joyride()
-                $('.em-preview-modal').show()
-                onFinish = (id) ->
-                   $('.tour-intro').hide()
-                   $('.tour-intro-step2').show()
-                iframe = $("#video-opening")[0]
-                player = $f(iframe)
-                player.addEvent "ready", ->
-                    player.addEvent "finish", onFinish
-            else
-                $('.modal-wrapper').hide()
+
+    isMobile = -> $(window).width() < 480 or $(window).height() < 480
+
+    startJoyride = ->
+        $("#joyride-steps").joyride()
+        #strange bug, if called once, it doesn't work
+        $("#joyride-steps").joyride()
+
+    showIntroVideo = ->
+
+    showIntro = ->
+        if !isMobile()
+
+            startJoyride()
+
+            $('.em-preview-modal').show()
+            iframe = $("#video-opening")[0]
+            player = $f(iframe)
+            player.addEvent "ready", ->
+                player.addEvent "finish", (id) ->
+                    $('.tour-intro').hide()
+                    $('.tour-intro-step2').show()
+        else
+            $('.modal-wrapper').hide()
+
+    showIntro()

     $('a.icon-close').click -> $('.modal-wrapper').hide()
     $('a.btn').click -> $('.modal-wrapper').hide()


Removed console log

Removed console log

--- app/assets/javascripts/users.coffee
+++ app/assets/javascripts/users.coffee
@@ -3,7 +3,6 @@
 # You can use CoffeeScript in this file: http://coffeescript.org/
 class UsersController
   new: ->
-    console.log "Hello!"
     $( '#formRegister' ).validate
       rules:
         username:


make unarchive more responsive by downloading documents in parallel

unarchive is triggered interactively so we should try to make it
reasonably fast

--- app/coffee/DocArchiveManager.coffee
+++ app/coffee/DocArchiveManager.coffee
@@ -47,7 +47,7 @@
 				return callback new Error("No docs for project #{project_id}")
 			jobs = _.map docs, (doc) ->
 				(cb)-> DocArchiveManager.unArchiveDocChanges project_id, doc._id, cb
-			async.series jobs, callback
+			async.parallelLimit jobs, 4, callback

 	unArchiveDocChanges: (project_id, doc_id, callback)->
 		MongoManager.getArchivedDocChanges doc_id, (error, count) ->


Debounce page loads for 200ms (fix #57)

Debounce page loads for 200ms (fix #57)

--- app/scripts/directives/pager.coffee
+++ app/scripts/directives/pager.coffee
@@ -25,26 +25,28 @@
     @elements.next = @element.find('.next')

   bindEvents: ->
+    onPageUpdate = $.debounce 200, => @options.onPageChange(@options.currentPage, @options.pageSize)
+
     @elements.pages.on 'click', 'a', (ev) =>
       link = $(ev.currentTarget)
       page = parseInt(link.attr('data-current-page'), 10)
       return if page == @options.currentPage
       @options.currentPage = page
-      @options.onPageChange(@options.currentPage, @options.pageSize)
+      onPageUpdate()
       @updatePager()

     @elements.previous.on 'click', (ev) =>
       return if @options.currentPage == 1
       @options.currentPage -= @options.visiblePageCount
       @options.currentPage = 1 if @options.currentPage < 2
-      @options.onPageChange(@options.currentPage, @options.pageSize)
+      onPageUpdate()
       @updatePager()

     @elements.next.on 'click', (ev) =>
       return if @options.pageCount? and @options.currentPage >= @options.pageCount
       @options.currentPage += @options.visiblePageCount
       @options.currentPage = @options.pageCount if @options.pageCount? and @options.currentPage > @options.pageCount
-      @options.onPageChange(@options.currentPage, @options.pageSize)
+      onPageUpdate()
       @updatePager()

   watchScope: ->


check cannot access profile when logged out

--- test/functional/profileTest.coffee
+++ test/functional/profileTest.coffee
@@ -41,3 +41,10 @@
             should.not.exist(err)
             assert.equal fakeUser.lastName, modifiedUser.lastName
             done()
+
+  describe '** Logged out **', ->
+    describe 'GET', ->
+      it 'should return 302', (done) ->
+        request(new App()).get('/profile').expect(302).end (err, res) ->
+          should.not.exist err
+          done()


Remove extra logging

Remove extra logging

--- src/smc-webapp/r_account.cjsx
+++ src/smc-webapp/r_account.cjsx
@@ -52,6 +52,9 @@

 {APIKeySetting} = require('./api-key')

+log_strategy = (is_anonymous, name) ->
+    if is_anonymous
+        log("add_passport", {passport: name, source: "anonymous_account"})

 # Define a component for working with the user's basic
 # account information.
@@ -487,7 +490,7 @@

             <ButtonToolbar style={textAlign: 'center'}>
                 <Button href={"#{window.app_base_url}/auth/#{@state.add_strategy_link}"} target="_blank"
-                    onClick={=>@setState(add_strategy_link:undefined); log("add_passport", {passport: name})}>
+                    onClick={=>@setState(add_strategy_link:undefined); log_strategy(@props.is_anonymous, name)}>
                     <Icon name="external-link" /> Link My {name} Account
                 </Button>
                 <Button onClick={=>@setState(add_strategy_link:undefined)} >
@@ -546,7 +549,7 @@
         if strategy != 'email'
             <Button
                 disabled={@props.is_anonymous and not @state.terms_checkbox}
-                onClick = {=>@setState(if strategy in strategies then {remove_strategy_button:strategy, add_strategy_link:undefined} else {add_strategy_link:strategy, remove_strategy_button:undefined}); log("toggle_open_passport", {passport: strategy, is_terms_checked: @state.terms_checkbox})}
+                onClick = {=>@setState(if strategy in strategies then {remove_strategy_button:strategy, add_strategy_link:undefined} else {add_strategy_link:strategy, remove_strategy_button:undefined})}
                 key     = {strategy}
                 bsStyle = {if strategy in strategies then 'info' else 'default'}>
                 <Icon name={strategy} /> {misc.capitalize(strategy)}...
@@ -671,7 +674,7 @@
             style.border = '2px solid red'
         <FormGroup style={ style }>
             <Checkbox
-              onChange={(e) => this.setState({ terms_checkbox: e.target.checked }); log("agree_to_terms_anonymous_account")}
+              onChange={(e) => this.setState({ terms_checkbox: e.target.checked })}
             >
                  <TermsOfService />
             </Checkbox>


refactor pluggable.initializePlugins

refactor pluggable.initializePlugins

--- core/pluggable.coffee
+++ core/pluggable.coffee
@@ -132,19 +132,18 @@
     else
       return false

-exports.initializePlugins = (callback) ->
-  checkDependencies = ->
-    all_plugins = _.union config.plugin.available_extensions, config.plugin.available_services
+exports.initializePlugins = ->
+  all_plugins = _.union config.plugin.available_extensions, config.plugin.available_services

-    for plugin_name in all_plugins
-      plugin = require path.join __dirname, "../plugin/#{plugin_name}"
+  for plugin_name in all_plugins
+    plugin = require path.join __dirname, "../plugin/#{plugin_name}"

-      if plugin.dependencies
-        for dependence in plugin.dependencies
-          unless dependence in all_plugins
-            throw new Error "#{plugin_name} is Dependent on #{dependence} but not load"
+    if plugin.dependencies
+      for dependence in plugin.dependencies
+        unless dependence in all_plugins
+          throw new Error "#{plugin_name} is Dependent on #{dependence} but not load"

-  initializePlugin = (name, callback) ->
+  for plugin_name in all_plugins
     plugin_path = path.join __dirname, "../plugin/#{name}"
     plugin = require plugin_path

@@ -153,30 +152,6 @@

     if fs.existsSync path.join(plugin_path, 'static')
       app.use harp.mount("/plugin/#{name}", path.join(plugin_path, 'static'))
-
-    callback plugin
-
-  initializeExtension = (plugin, callback) ->
-    callback()
-
-  initializeService = (plugin, callback) ->
-    callback()
-
-  checkDependencies()
-
-  async.parallel [
-    (callback) ->
-      async.each config.plugin.available_extensions, (name, callback) ->
-        initializePlugin name, (plugin) ->
-          initializeExtension plugin, callback
-      , callback
-
-    (callback) ->
-      async.each config.plugin.available_services, (name, callback) ->
-        initializePlugin name, (plugin) ->
-          initializeService plugin, callback
-      , callback
-  ], callback

 exports.createHelpers = (plugin) ->
   plugin.registerHook = (hook_name, payload) ->


chore(webpack): define resolve fallback

chore(webpack): define resolve fallback

--- webpack.config.coffee
+++ webpack.config.coffee
@@ -70,6 +70,9 @@
   resolve:
     extensions: ["", ".js", ".coffee"]
     modulesDirectories: ['node_modules']
+    fallback: process.env.NODE_PATH
+  resolveLoader:
+    fallback: process.env.NODE_PATH
   stylus:
     use: [(require "nib")()]


Added phrasing trigger

Added phrasing trigger

--- support/hardcoded_triggers.coffee
+++ support/hardcoded_triggers.coffee
@@ -41,6 +41,7 @@
   '!goal'         : "GOOOOOOOOOOOOOOOOOOOOOOAAAAAAAAAAAAAAAAALLLLLLLLLL!!!!!!!"
   '!unstoppable'  : "I am unstoppable!!! http://i.imgur.com/ALHS4Za.png"
   '!haha'         : "http://i.imgur.com/CVhoCwr.jpg"
+  '!phrasing'     : "Phrasing! http://batdoc.files.wordpress.com/2012/03/archer-1-phrasing.jpg"

 # Bot Info
 nick = process.env.HUBOT_IRC_NICK?.toLowerCase() ? "hubot"


Fixed callback.

Fixed callback.

--- vendor/assets/javascripts/jquery.screenfull.js.coffee
+++ vendor/assets/javascripts/jquery.screenfull.js.coffee
@@ -8,5 +8,5 @@
     fullScreen: ->
       $(@).on 'click', ->
         screenfull.toggle(@) if $.isFullScreenEnabled()
-      .on screenfull.raw.fullscreenchange ->
+      .on screenfull.raw.fullscreenchange, ->
         $(@).trigger(if screenfull.isFullscreen then 'fullscreen:enter' else 'fullscreen:exit')


fix #14

fix #14

--- lib/clang-format.coffee
+++ lib/clang-format.coffee
@@ -20,7 +20,7 @@
   handleBufferEvents: (editor) ->
     buffer = editor.getBuffer()
     buffer.onDidSave =>
-      scope = editor.getLastCursor().getScopeDescriptor().scopes[0]
+      scope = editor.getRootScopeDescriptor().scopes[0]
       if atom.config.get('clang-format.formatOnSave') and scope in ['source.c++', 'source.cpp']
         @format(editor)


add coverage

add coverage

--- test/test_util.coffee
+++ test/test_util.coffee
@@ -13,6 +13,16 @@
     ,done
     return

+  it 'wrap', (done)->
+    util.wrap (()->
+      perr "SHALL NOT PASS"
+      pp   "SHALL NOT PASS"
+      p    "SHALL NOT PASS"
+      puts "SHALL NOT PASS"
+      process.exit()
+    ),done
+    return
+
   it 'throws', (done)->
     util.throws ()->
       throw new Error "test"
@@ -26,4 +36,10 @@

     return

+
+  it 'not_throws not throws', ()->
+    util.not_throws ()->
+
+    return
+


Reports the Docker-generated name for containers

Useful for when a run command is specified and we let Docker create the
container name for us to prevent collisions.

--- lib/commands/run.coffee
+++ lib/commands/run.coffee
@@ -304,6 +304,8 @@
     DockerUtils.inspectContainer(container)
   .then ({container, info}) ->
     options.reporter.succeedTask()
+    if serviceConfig.containerName is ''
+      options.reporter.completeTask "#{chalk.gray 'Running as:'} #{chalk.bold info.Name.substring(1)}"
     {container, info}

 # If options.attach is true, calls DockerUtils.attachContainer to get a stream. We do this before
@@ -959,4 +961,3 @@

 # Exposed for unit testing
 module.exports.parseArgs = parseArgs
-


fix scrolling past end

fix scrolling past end

--- lib/console/view.coffee
+++ lib/console/view.coffee
@@ -123,6 +123,8 @@

   inputView: (item) ->
     ed = document.createElement 'atom-text-editor'
+    # Wait for ed to be attached
+    setTimeout (-> ed.component.presenter.scrollPastEnd = false), 0
     item.editor = ed.getModel()
     item.editor.setLineNumberGutterVisible(false)
     @updateGrammar item


Passing failures along quit method

Passing failures along quit method

--- tests/www/tests/utils/hooks.coffee
+++ tests/www/tests/utils/hooks.coffee
@@ -27,7 +27,7 @@
   ctx.afterAll (done)->

     if not coverage
-      return quit browser, notify_sauce_labs, done
+      return quit browser, failures, notify_sauce_labs, done

     console.log '\n-----\nGenerating test coverage\n-----'

@@ -35,7 +35,7 @@
       should.not.exist err
       should.exist coverage
       post_coverage base_url, coverage, ->
-        quit browser, notify_sauce_labs, done
+        quit browser, failures, notify_sauce_labs, done

   ctx.beforeEach ->
     passed = false
@@ -48,7 +48,7 @@
     do done

-quit = (browser, notify_sauce_labs, done) ->
+quit = (browser, failures, notify_sauce_labs, done) ->
   browser.quit (err)->
     should.not.exist err


Fix list sorting

Fix list sorting

--- app/controllers/tasks.coffee
+++ app/controllers/tasks.coffee
@@ -110,7 +110,9 @@
     @el.removeClass "empty"

     # Update current list if the list is changed
-    @list = list if list
+    # hackery hack for completed & all. fuckit, we're shipping
+    if list instanceof List or list.id is "all" or list.id is "completed"
+      @list = list

     #Something
     if @list.disabled


Run copy-i18n-tags (#5868)

Run copy-i18n-tags (#5868)

Co-authored-by: CoCo CI <5df1af3d7600b97ee69552caffeccf9dfd29ab8f@codecombat.com>

--- app/locale/nl-BE.coco.coffee
+++ app/locale/nl-BE.coco.coffee
@@ -1081,7 +1081,7 @@
 #    editor_config_behaviors_label: "Smart Behaviors"
 #    editor_config_behaviors_description: "Autocompletes brackets, braces, and quotes."

-#   about:
+#  about:
 #    title: "About CodeCombat - Engaging Students, Empowering Teachers, Inspiring Creation"
 #    meta_description: "Our mission is to level computer science through game-based learning and make coding accessible to every learner. We believe programming is magic and want learners to be empowered to to create things from pure imagination."
 #    learn_more: "Learn More"


add grunt fail for missing emc

add grunt fail for missing emc

--- lib/grunt/module.coffee
+++ lib/grunt/module.coffee
@@ -137,6 +137,9 @@

   # @nodoc
   _buildDependencies: ->
+    if _.isEmpty(@getEmc()) || _.isEmpty(@getEmc().emc)
+      @grunt.fail.fatal "Missing config for module #{@getModName()} in env #{@getEnvName()}"
+
     @_dependencies = _.map (@getEmc().emc.dependencies || []), (mod_name) =>
       @grunt.graspi.config.getModule(@getEnvName(), mod_name)


Run copy-i18n-tags (#6267)

Run copy-i18n-tags (#6267)

Co-authored-by: CoCo CI <5df1af3d7600b97ee69552caffeccf9dfd29ab8f@codecombat.com>

--- app/locale/rot13.coco.coffee
+++ app/locale/rot13.coco.coffee
@@ -941,12 +941,12 @@
     feature_heroes: "Haybpx rkpyhfvir urebrf naq crgf"
     feature_learn: "Yrnea gb znxr tnzrf naq jrofvgrf"
     feature_gems: "Erprvir __trzf__ trzf cre zbagu"
-    month_price: "$__cevpr__/zb" # {punatr}
+    month_price: "$__cevpr__/zb"
     first_month_price: "Bayl $__cevpr__ sbe lbhe svefg zbagu!"
     lifetime: "Yvsrgvzr Npprff"
     lifetime_price: "$__cevpr__"
     year_subscription: "Naahny Fhofpevcgvba"
-    year_price: "$__cevpr__/le" # {punatr}
+    year_price: "$__cevpr__/le"
     support_part1: "Arrq uryc jvgu cnlzrag be cersre CnlCny? Rznvy"
     support_part2: "fhccbeg@pbqrpbzong.pbz"


Only mark unread notifications as read

Only mark unread notifications as read

--- app/pages/notifications.cjsx
+++ app/pages/notifications.cjsx
@@ -18,6 +18,7 @@
   getInitialState: ->
     firstMeta: { }
     lastMeta: { }
+    notificationsMap: { }

   componentWillMount: ->
     @getNotifications() if @props.user
@@ -40,6 +41,10 @@
       meta = newNotifications[0]?.getMeta() or { }
       notifications = @state.notifications or newNotifications
       meta.notificationIds = (n.id for n in newNotifications)
+      notificationsMap = @state.notificationsMap
+
+      for notification in newNotifications
+        notificationsMap[notification.id] = notification

       {firstMeta, lastMeta} = @state

@@ -57,6 +62,8 @@
   markAsRead: (meta)->
     =>
       ids = @state["#{ meta }Meta"].notificationIds
+      ids = (id for id in ids when not @state.notificationsMap[id].delivered)
+      return if ids.length is 0
       setTimeout =>
         talkClient.put '/notifications/read', id: ids.join(',')
         for notification in @state.notifications when notification.id in ids


proper event cleanup

proper event cleanup

--- app/views/tool_window.coffee
+++ app/views/tool_window.coffee
@@ -118,7 +118,7 @@
   resizeWindowEnd: =>
     $('body').removeClass 'unselectable'
     @resizing = false
-    $(document).off 'mousemove'
+    $(document).off 'mousemove mouseup'

     @model.set
       left: @$el.css('left')


Don't cancel search mini-editor on blur(app-switch by cmd-tab) #539

--- lib/search-input.coffee
+++ lib/search-input.coffee
@@ -45,7 +45,6 @@
     atom.commands.add @editorElement,
       'core:confirm': => @confirm()
       'core:cancel': => @cancel()
-      'blur': => @cancel() unless @finished
       'vim-mode-plus:input-cancel': => @cancel()

   focus: (@options={}) ->


Implement shouldAllowDrag in positive logic for tabs

--- lib/tab-bar-view.coffee
+++ lib/tab-bar-view.coffee
@@ -86,9 +86,8 @@
     for session in editor.editSessions
       return true if editSession.getPath() is session.getPath()

-  shouldAllowDrag: (event) ->
-    panes = rootView.find('.pane')
-    !(panes.length == 1 && panes.find('.sortable').length == 1)
+  shouldAllowDrag: ->
+    (@paneContainer.getPanes().length > 1) or (@pane.getItems().length > 1)

   onDragStart: (event) =>
     super


Fix 'select-a-word' command not behaving correctly when cursor is on the first character of a word.

--- lib/text-objects.coffee
+++ lib/text-objects.coffee
@@ -112,7 +112,7 @@

 class SelectAWord extends TextObject
   select: ->
-    @editor.moveCursorToBeginningOfWord()
+    @editor.selectWord()
     @editor.selectToBeginningOfNextWord()
     [true]


Fixed passing in variable

Fixed passing in variable

--- core/app/backbone/initializers/client_initializer.coffee
+++ core/app/backbone/initializers/client_initializer.coffee
@@ -67,13 +67,11 @@
       e.stopPropagation()
       app.vent.trigger 'close_discussion_sidebar'

-  clientEnvoy = new ClientEnvoy(senderEnvoy)
-
   if window.parent && window != window.parent
     senderEnvoy = Factlink.createSenderEnvoy(window.parent)
-    Factlink.createReceiverEnvoy clientEnvoy
+    Factlink.createReceiverEnvoy new ClientEnvoy(senderEnvoy)
   else
     senderEnvoy = (args...) -> console.info 'senderEnvoy:', args...
-    initDevelopmentConsole(clientEnvoy)
+    initDevelopmentConsole new ClientEnvoy(senderEnvoy)

   senderEnvoy 'modalFrameReady'


Return handles.

Return handles.

--- lib.coffee
+++ lib.coffee
@@ -516,9 +516,13 @@
     @exportedVariables?[name]

   subscribe: (args...) =>
+    subscription = Meteor.subscribe args...
+
     # Store subscription so we can unsubscribe from everything on tear down.
     @_internal.subscriptions ?= []
-    @_internal.subscriptions.push Meteor.subscribe args...
+    @_internal.subscriptions.push subscription
+
+    subscription

   unsubscribeAll: =>
     # Unsubscribe from everything the test cases subscribed to.
@@ -526,5 +530,9 @@
     @_internal.subscriptions = []

   autorun: (handler) =>
+    computation = Tracker.autorun _.bind handler, @
+
     @_internal.computations ?= []
-    @_internal.computations.push Tracker.autorun _.bind handler, @
+    @_internal.computations.push computation
+
+    computation


Added leading whitespace to default list

--- src/js/settings.js.coffee
+++ src/js/settings.js.coffee
@@ -17,6 +17,7 @@
       "zerzan":                       TIMESTAMP
   regex:
     name:
+      "^\\s":                         TIMESTAMP
       "\\bWI\\b":                     TIMESTAMP
       "^.+heller|heller.+$":          TIMESTAMP
       "bi.*lover":                    TIMESTAMP


Kite: Added event emitters for kite errors.

--- client/app/MainApp/kite/kitecontroller.coffee
+++ client/app/MainApp/kite/kitecontroller.coffee
@@ -31,6 +31,7 @@
   createKite:(kiteName, correlationName, kiteKey)->
     kite = new Kite { kiteName, correlationName, kiteKey }
     kite.on 'destroy', => @destroyKite kite
+    kite.on "KiteError", (err) => @emit "KiteError", err
     return kite

   # notification = null


removed conflicted file

removed conflicted file

--- client/app/MainApp/sidebar/groupavatar.coffee
+++ client/app/MainApp/sidebar/groupavatar.coffee
@@ -1,26 +0,0 @@
-class GroupAvatar extends JView
-
-  constructor:(options = {}, data)->
-
-    options.cssClass = 'group-avatar-drop hidden'
-    groupsController = KD.getSingleton 'groupsController'
-    super options, groupsController.getCurrentGroupData()
-
-    groupsController.on 'GroupChanged', @bound 'render'
-
-  render:(slug, group)->
-    if group
-
-      @$().css backgroundImage : \
-        "url(#{group.avatar or 'http://lorempixel.com/60/60/?' + @utils.getRandomNumber()})"
-
-      @setTooltip
-        title : """You are now in #{group.title} group.
-
 Click here to see group's homepage."""
-
-      @show() unless slug is 'koding'
-
-  click:->
-    super
-    if KD.config.entryPoint?.type? == "group"
-      KD.getSingleton('lazyDomController').showLandingPage()


Copy edits

Copy edits

--- static/embed/oab_embed.coffee
+++ static/embed/oab_embed.coffee
@@ -224,7 +224,7 @@

 _oab.prototype.cml = () -> return this.config.problem ? this.config.owner ? this.config.email ? ''
-_oab.prototype.contact = () -> return 'Please try ' + (if this.cml() then 'contacting your library' else 'contacting your library') + ' directly'
+_oab.prototype.contact = () -> return 'Please try ' + (if this.cml() then 'contacting us' else 'contacting us') + ' directly'

 _oab.prototype.loading = (load) ->
   _L.hide '#_oab_error'
@@ -981,7 +981,7 @@

Your paper is already freely available!

Great news, you\'re already getting the benefits of sharing your work! Your publisher or co-author have already shared a freely available copy.

-
Give us your email to confirm deposit

+
Please enter your email to confirm deposit

@@ -1047,7 +1047,7 @@

We\'ll double check your paper

-
You\'ve done your part for now. Hopefully, we\'ll send you a link soon. First, we\'ll check in the next working day to make sure it\'s legal to share.

+
You\'ve done your part for now. Hopefully, we\'ll send you a link soon. First, we\'ll check to make sure it\'s legal to share.


collaboration: use destroyChannel and detachSocialChannel helpers

--- client/ide/lib/collaborationcontroller.coffee
+++ client/ide/lib/collaborationcontroller.coffee
@@ -88,20 +88,11 @@
     {channel}    = kd.getSingleton 'socialapi'
     {JWorkspace} = remote.api

-    options = channelId: @socialChannel.getId()
-    channel.delete options, (err) =>
-
+    socialHelpers.destroyChannel @socialChannel, (err) =>
       return callback err  if err

-      @channelId = @socialChannel = null
-
-      options = $unset: channelId: 1
-      JWorkspace.update @workspaceData._id, options, (err) =>
-
-        return callback err  if err
-
-        @workspaceData.channelId = null
-        callback null
+      @unsetSocialChannel()
+      workspaceHelpers.detachSocialChannel @workspaceData, callback

   # FIXME: This method is called more than once. It should cache the result and


Collaboration: Reformat send ping method

--- client/ide/lib/collaborationcontroller.coffee
+++ client/ide/lib/collaborationcontroller.coffee
@@ -536,30 +536,33 @@

   startHeartbeat: ->
+
     interval = 1000 * 15
     @sendPing() # send the first ping
     @pingInterval =  kd.utils.repeat interval, @bound 'sendPing'

   sendPing: ->
+
     {channelId} = @workspaceData
-    doXhrRequest {
-      # generate data
-      data     : {
+
+    doXhrRequest
+      endPoint : '/api/social/collaboration/ping'
+      type     : 'POST'
+      async    : yes
+      data:
         fileId    : @rtmFileId
         channelId : channelId
-      }
-      type     : 'POST'
-      async    : yes
-      endPoint : '/api/social/collaboration/ping'
-    }, (err, response) ->
+    , (err, response) ->
+
       return  if not err

-      console.error err
-
-      if err.code == 400
+      if err.code is 400
         kd.utils.killRepeat @pingInterval # graceful stop
         throwError "bad request, err: %s", err.message
+      else
+        throwError "#{err}: %s", JSON.stringify response
+

   forceQuitCollaboration: ->


sidebar section number

sidebar section number

--- docs/components/docs/DocsBody.cjsx
+++ docs/components/docs/DocsBody.cjsx
@@ -54,8 +54,9 @@

             { for fileName, file of docs
+                sectionNumber = if fileName.split('-')[0].indexOf('.') is -1 then fileName.split('-')[0] else ''
                 <Tile key={ fileName }>
-

+
{ sectionNumber }

{ /title: (.+)/.exec(file)[1] }

                 </Tile> }
@@ -72,7 +73,7 @@
                     <Markdown>{ body }</Markdown>

                 else
-
{ title }
 }
+
{ title }
 }

         </Grid>


Better Metaphysics debugging

Better Metaphysics debugging

--- apps/artwork_2/routes.coffee
+++ apps/artwork_2/routes.coffee
@@ -1,4 +1,7 @@
+{ extend } = require 'underscore'
+qs = require 'qs'
 metaphysics = require '../../lib/metaphysics'
+{ METAPHYSICS_ENDPOINT } = require('sharify').data

 query = """
   query artwork($id: String!) {
@@ -14,16 +17,13 @@
 """

 @index = (req, res, next) ->
-  metaphysics
-    variables: id: req.params.id
-    query: query
+  send = query: query, variables: id: req.params.id

-  .then (data) ->
-    if req.query.query?
-      res.send query
-    else if req.query.data?
-      res.send data
-    else
+  if req.query.query?
+    get = extend {}, send, variables: JSON.stringify send.variables
+    return res.redirect "#{METAPHYSICS_ENDPOINT}?#{qs.stringify get}"
+
+  metaphysics send
+    .then (data) ->
       res.render 'index', data
-
-  .catch next
+    .catch next


Don't test window coordinates in popup test

--- test/anvil-connect.angular.coffee
+++ test/anvil-connect.angular.coffee
@@ -177,8 +177,6 @@
       popup = Anvil.popup(700, 500)
       expect(popup).toContain 'width=700,'
       expect(popup).toContain 'height=500,'
-      expect(popup).toContain 'left=197,'
-      expect(popup).toContain 'top=176,'
       expect(popup).toContain 'dialog=yes,'
       expect(popup).toContain 'dependent=yes,'
       expect(popup).toContain 'scrollbars=yes,'


more examples on lazy dependencies

more examples on lazy dependencies

--- test/examples/general_test.coffee
+++ test/examples/general_test.coffee
@@ -83,8 +83,18 @@
     it "injects the value", (user) -> expect(user).eq("ID - 10")

   describe "Lazy Caching", ->
-    lazy "random", -> Math.random()
+    lazy "random", -> Q(Math.random()).delay(30)
     lazy "urand", (random) -> "U-#{random}"

     it "should use the same", (urand, random) ->
       expect(urand).eq("U-#{random}")
+
+  describe "Lazy Lookup", ->
+    lazy "onroot", -> Q("root").delay(20)
+    lazy "onchild", -> "rootChild"
+
+    describe "inner block", ->
+      lazy "onchild", -> "child"
+
+      it "loads the child first", (onchild) -> expect(onchild).eq("child")
+      it "loads from proper parents", (onroot) -> expect(onroot).eq("root")


Clean up mergeSuggestionsFromProviders

Clean up mergeSuggestionsFromProviders

--- lib/autocomplete-manager.coffee
+++ lib/autocomplete-manager.coffee
@@ -131,9 +131,8 @@
   mergeSuggestionsFromProviders: (providers, providerSuggestions) ->
     providerSuggestions.reduce (suggestions, providerSuggestions, index) ->
       provider = providers[index]
-
-      return suggestions unless providerSuggestions?.length
-      suggestions.concat(providerSuggestions)
+      suggestions = suggestions.concat(providerSuggestions) if providerSuggestions?.length
+      suggestions
     , []

   displaySuggestions: (suggestions, suggestionsPromise, options) =>


Debugging

Debugging

--- src/lib/product.js.coffee
+++ src/lib/product.js.coffee
@@ -197,6 +197,7 @@
       params =
         route: "products/#{@id}"
         body:  this._toJSON()
+      console.debug params
       Firehose.client.put( this, params )
     else
       params =


KDTokenizedInputView: skip DOM manipulation procedures if token input is not in DOM

--- components/inputs/KDTokenizedInputView.coffee
+++ components/inputs/KDTokenizedInputView.coffee
@@ -119,11 +119,12 @@
         else @matchPrefix()

   cancel: ->
-    text = document.createTextNode @tokenInput.textContent
-    @getEditableElement().insertBefore text, @tokenInput
-    @tokenInput.nextSibling.remove()
-    @tokenInput.remove()
-    @utils.selectEnd text
+    if @tokenInput.parentNode
+      text = document.createTextNode @tokenInput.textContent
+      @getEditableElement().insertBefore text, @tokenInput
+      @tokenInput.nextSibling.remove()
+      @tokenInput.remove()
+      @utils.selectEnd text
     @hideMenu()

   reset: ->


#550 dirty workaround for cursor visibility get wrong in ctrl-f in vL

It begins after I call updateSync() in scroll motion.

--- lib/cursor-style-manager.coffee
+++ lib/cursor-style-manager.coffee
@@ -77,7 +77,10 @@
     # But corresponding cursorsComponent(HTML element) is added in sync.
     # So to modify style of cursorsComponent, we have to make sure corresponding cursorsComponent
     # is available by component in sync to model.
-    @editorElement.component.updateSync() if submode in ['characterwise', 'blockwise']
+    # [FIXME]
+    # When ctrl-f, b, d, u in vL mode, I had to call updateSync to show cursor correctly
+    # But it wasn't necessary before I iintroduce `moveToFirstCharacterOnVerticalMotion` for `ctrl-f`
+    @editorElement.component.updateSync()

     # [FIXME] In spec mode, we skip here since not all spec have dom attached.
     return if isSpecMode


showing landing page if not current user

--- app/assets/javascripts/backbone/apps/ticket/showbycat/showbycat_controller.js.coffee
+++ app/assets/javascripts/backbone/apps/ticket/showbycat/showbycat_controller.js.coffee
@@ -4,17 +4,22 @@

     initialize: (options) ->
       {category, id} = options
-      id = category.get("id") if category
-      tickets = App.request "ticket:entities", id
-      category or= App.request "category:entity", id
+      currentUser = App.request "get:current:user"
+      currentUserId = currentUser.get("id")
+      if currentUserId
+        id = category.get("id") if category
+        tickets = App.request "ticket:entities", id
+        category or= App.request "category:entity", id

-      @layout = @getLayoutView tickets, category
+        @layout = @getLayoutView tickets, category

-      @listenTo @layout, "show", =>
-        @ticketRegion(tickets)
-        @titleRegion(category)
+        @listenTo @layout, "show", =>
+          @ticketRegion(tickets)
+          @titleRegion(category)

-      @show @layout, loading: true
+        @show @layout, loading: true
+      else
+        App.vent.trigger "landing:page:show"

     ticketRegion: (tickets) ->
       ticketsView = @getTicketsView tickets


Use loaded packages and remove call count

--- lib/deprecation-cop-view.coffee
+++ lib/deprecation-cop-view.coffee
@@ -146,7 +146,7 @@
     return

   createIssueUrl: (packageName, deprecation, stack) ->
-    return unless repo = atom.packages.getActivePackage(packageName)?.metadata?.repository
+    return unless repo = atom.packages.getLoadedPackage(packageName)?.metadata?.repository
     repoUrl = repo.url ? repo
     repoUrl = repoUrl.replace(/\.git$/, '')

@@ -156,7 +156,7 @@
     "#{repoUrl}/issues/new?title=#{encodeURI(title)}&body=#{encodeURI(body)}"

   createSelectorIssueUrl: (packageName, deprecation, sourcePath) ->
-    return unless repo = atom.packages.getActivePackage(packageName)?.metadata?.repository
+    return unless repo = atom.packages.getLoadedPackage(packageName)?.metadata?.repository
     repoUrl = repo.url ? repo
     repoUrl = repoUrl.replace(/\.git$/, '')

@@ -208,7 +208,6 @@
                     @raw marked(deprecation.getMessage())

                   @div class: 'btn-toolbar', =>
-                    @span "Called #{_.pluralize(stack.callCount, 'time')}"
                     if packageName and url = self.createIssueUrl(packageName, deprecation, stack)
                       @a class: 'issue-url', href: url, "Create Issue on #{packageName} repo"


Implemented multi parameter route matching tests

--- tests/match-routes-tests.coffee
+++ tests/match-routes-tests.coffee
@@ -111,10 +111,33 @@

 	## Multiple parameters

-	it 'matches a route with multiple parameters', () -> expect(true).toBe(false)
-	it 'matches a route with multiple parameters and a query string', () -> expect(true).toBe(false)
-	it 'matches a route with multiple parameters and a hash', () -> expect(true).toBe(false)
-	it 'matches a route with a parameter and a hash and a query string', () -> expect(true).toBe(false)
+	it 'matches a route with multiple parameters', () ->
+		router.go '/foo/123/bar/abc'
+
+		expect(router.current().component).toBe('multiparameters')
+		expect(router.current().parameters.foo).toBe('123')
+		expect(router.current().parameters.bar).toBe('abc')
+
+	it 'matches a route with multiple parameters and a query string', () ->
+		router.go '/foo/123/bar/abc?foo=bar'
+
+		expect(router.current().component).toBe('multiparameters')
+		expect(router.current().parameters.foo).toBe('123')
+		expect(router.current().parameters.bar).toBe('abc')
+
+	it 'matches a route with multiple parameters and a hash', () ->
+		router.go '/foo/123/bar/abc#hash'
+
+		expect(router.current().component).toBe('multiparameters')
+		expect(router.current().parameters.foo).toBe('123')
+		expect(router.current().parameters.bar).toBe('abc')
+
+	it 'matches a route with a parameter and a hash and a query string', () ->
+		router.go '/foo/123/bar/abc#hash?foo=bar'
+
+		expect(router.current().component).toBe('multiparameters')
+		expect(router.current().parameters.foo).toBe('123')
+		expect(router.current().parameters.bar).toBe('abc')

 	## Unknown route


fixing paths in gulpfile

fixing paths in gulpfile

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -72,7 +72,7 @@

 gulp.task "css:ref", ->
-  return gulp.src "./templates/includes/head.jade", {base: "./"}
+  return gulp.src "./templates/includes/head.jade"
           .pipe rev_replace( { manifest: gulp.src("assets/rev_manifest/rev-manifest.json"), replaceInExtensions: ['.jade']} )
           .pipe gulp.dest "./"

@@ -211,10 +211,10 @@
     trellisQuantisation: false
   }

-  return gulp.src('build/media/images/**/*.{jpg,png}',{base: './'})
+  return gulp.src './build/media/images/**/*.{jpg,png}'
     .pipe(plumber())
     .pipe(image_min(options))
-    .pipe(gulp.dest('./'))
+    .pipe gulp.dest './'

 # =================================================================


Gulpfile: use playground/main.coffee for entry_path, add play task which builds and watches playground

--- gulpfile.coffee
+++ gulpfile.coffee
@@ -18,7 +18,7 @@
 {exec}     = require 'child_process'

 STYLES_PATH = require './src/themes/styl.includes.coffee'
-ENTRY_PATH  = ['./src/init.coffee']
+ENTRY_PATH  = ['./playground/main.coffee']
 COFFEE_PATH = ['./src/components/**/*.coffee','./src/core/**/*.coffee','./src/init.coffee']
 LIBS        = require './src/lib.includes.coffee'

@@ -106,13 +106,24 @@

 gulp.task 'html', ->
+gulp.task 'play', ->

   try fs.mkdirSync './build'
+  stream = gulp.src(ENTRY_PATH, { read: false })
+    .pipe(browserify
+      transform   : ['coffeeify']
+      extensions  : ['.coffee']
+      debug       : yes )
+    .pipe(concat "main.js")
+    .pipe(gulp.dest "playground/js")

   fs.writeFileSync './build/index.html', fs.readFileSync './src/index.html'
+  stream.pipe(livereload())  if useLiveReload

   gulp.src('./src/index.html').pipe(livereload())  if useLiveReload

+  if useLiveReload
+    gulp.src('./playground/index.html').pipe(livereload())

 gulp.task 'default', ['live', 'styles', 'libs', 'coffee', 'html', \
                       'watch-html', 'watch-styles', 'watch-coffee', 'watch-libs'], ->


Do not send e-mail if related content was deleted

--- workers/emailnotifications/main.coffee
+++ workers/emailnotifications/main.coffee
@@ -43,27 +43,33 @@
     definition      : "private messages"

 sendDailyEmail = (details, content)->
-  # console.log template.dailyMail details, content
-
-  Emailer.send
-    To        : emailWorker.defaultRecepient or details.email
-    Subject   : template.dailyHeader details
-    HtmlBody  : template.dailyMail details, content
-  , (err, status)->
-    log "An error occured: #{err}" if err
-    log "Daily e-mail sent to #{details.email}"
+  unless content or details.email
+    log "Content not found, notification postponed"
+  else
+    Emailer.send
+      To        : emailWorker.defaultRecepient or details.email
+      Subject   : template.dailyHeader details
+      HtmlBody  : template.dailyMail details, content
+    , (err, status)->
+      log "An error occured: #{err}" if err
+      log "Daily e-mail sent to #{details.email}"

 sendInstantEmail = (details)->
   {notification} = details

-  Emailer.send
-    To        : emailWorker.defaultRecepient or details.email
-    Subject   : template.commonHeader details
-    HtmlBody  : flags[details.key].template details
-  , (err, status)->
-    log "An error occured: #{err}" if err
-    notification.update $set: status: 'attempted', (err)->
-      console.error err if err
+  if details.realContent?.deletedAt? or not details.email?
+    log "Content not found, notification postponed"
+    notification.update $set: status: 'postponed', (err)->
+      console.error if err
+  else
+    Emailer.send
+      To        : emailWorker.defaultRecepient or details.email
+      Subject   : template.commonHeader details
+      HtmlBody  : flags[details.key].template details
+    , (err, status)->
+      log "An error occured: #{err}" if err
+      notification.update $set: status: 'attempted', (err)->
+        console.error err if err

 fetchSubjectContent = (contents, callback)->
   {constructorName} = contents.subject


SocialWorker: code style fixes

SocialWorker: code style fixes

--- workers/social/testhelper/index.coffee
+++ workers/social/testhelper/index.coffee
@@ -21,7 +21,7 @@
   bongo = new Bongo
     root   : __dirname
     mongo  : mongo
-    models :  '../lib/social/models'
+    models : '../lib/social/models'

   bongo.once 'dbClientReady', ->
     callback()
@@ -135,11 +135,11 @@
 generateDummyUserFormData = (opts = {}) ->

   dummyUserFormData =
-    email                     : generateRandomEmail()
-    agree                     : 'on'
-    password                  : 'testpass'
-    username                  : generateRandomUsername()
-    passwordConfirm           : 'testpass'
+    email           : generateRandomEmail()
+    agree           : 'on'
+    password        : 'testpass'
+    username        : generateRandomUsername()
+    passwordConfirm : 'testpass'

   dummyUserFormData = _.extend dummyUserFormData, opts

@@ -164,7 +164,7 @@
 generateUserInfo = (opts = {}) ->

   userInfo =
-    email           : "kodingtestuser+#{generateRandomString()}@gmail.com"
+    email           : generateRandomEmail()
     username        : generateRandomUsername()
     password        : 'testpass'
     lastName        : 'user'
@@ -203,9 +203,9 @@
       expect(err?.message).to.be.equal 'Access denied'
       callback()

-    if    args.length > 0
-    then  caller[callee] client, args..., kallback
-    else  caller[callee] client, kallback
+    if   args.length > 0
+    then caller[callee] client, args..., kallback
+    else caller[callee] client, kallback

 fetchRelation = (options, callback) ->
@@ -249,7 +249,7 @@
   toNotExist : (options, callback) ->
     fetchRelation options, (relationship) ->
       expect(relationship).to.not.exist
-      callback relationship
+      callback()

 }


nginx: add /-/ip and /-/ipcheck/:port routes

--- deployment/nginx.coffee
+++ deployment/nginx.coffee
@@ -247,6 +247,8 @@

     #{createHealthcheck(KONFIG)}

+    #{createIPRoutes(KONFIG)}
+
     # start server
     server {
       # we should not timeout on proxy connections
@@ -426,6 +428,23 @@
     }
   """

+createIPRoutes = (KONFIG) ->
+  return "" if not isProxy KONFIG.ebEnvName
+
+  return """
+    location ~^/-/ip$ {
+      return 200 $remote_addr;
+    }
+
+    location ~^/-/ipcheck/(?<port>\d+)(?<rest>.*)$ {
+      proxy_pass http://$remote_addr:$port$rest;
+      proxy_connect_timeout 5s;
+      proxy_read_timeout 5s;
+      proxy_send_timeout 5s;
+      proxy_next_upstream error timeout invalid_header;
+    }
+   """
+
 createListenDirective = (KONFIG, env) ->
   return "listen #{KONFIG.publicPort};" if not isProxy KONFIG.ebEnvName


[misc] add note about tests

[misc] add note about tests

--- modules/nunjucks-extensions.coffee
+++ modules/nunjucks-extensions.coffee
@@ -143,6 +143,7 @@
    * following that breadcrumb. It is a way to force one page at specific path to think and behave like
    * it is another page. In past, it was useful to workaround tricky structures, but for now it
    * exists mostly as extravagant scientific proof of some strange techniques.
+   * @todo Add tests, ya know
    * @todo Consider removing of breadcrumb overriding, think out of scenarios — is it useful or no
    * @todo Do not invoke `getPage` if there is no `@ctx.PAGE.breadcrumb`, since then all properties
    *       already available under built `@ctx.PAGE.props`. For now we can't do this, since


fix for answering profile questions

fix for answering profile questions

--- @client/auth.coffee
+++ @client/auth.coffee
@@ -729,7 +729,7 @@
     auth = fetch('auth')

     if auth.ask_questions && auth.form in \
-          ['edit profile', 'create account', 'create account via invitation']
+          ['edit profile', 'create account', 'create account via invitation', 'user questions']
       questions = customization('auth_questions')
     else
       questions = []
@@ -830,7 +830,7 @@
     # Client side validation of user questions
     # Note that we don't have server side validation because
     # the questions are all defined on the client.
-    if auth.ask_questions
+    if auth.ask_questions && auth.form in ['create account', 'create account via invitation']
       questions = customization('auth_questions')
       @local.errors = []
       for question in questions


We now have a linux queue instead of jvmotp, rails and spree

--- assets/scripts/travis.coffee
+++ assets/scripts/travis.coffee
@@ -54,9 +54,7 @@
   QUEUES: [
     { name: 'common',  display: 'Common' }
     { name: 'php',     display: 'PHP, Perl and Python' }
-    { name: 'jvmotp',  display: 'JVM, Erlang and Node.js' }
-    { name: 'rails',   display: 'Rails' }
-    { name: 'spree',   display: 'Spree' }
+    { name: 'linux',   display: 'Linux' }
   ]

   INTERVALS: { sponsors: -1, times: -1, updateTimes: 1000 }


use console.info

use console.info

--- demo/scripts/advanced.coffee
+++ demo/scripts/advanced.coffee
@@ -21,12 +21,12 @@
 cursorManager.setCursor('basic', 0, 'basic', 'rgba(255,153,51,0.9)')

 basicEditor.on('selection-change', (range) ->
-  console.log 'basic', 'selection', range
+  console.info 'basic', 'selection', range
   cursorManager.moveCursor('basic', range.end) if range?
 )

 basicEditor.on('text-change', (delta, source) ->
-  console.log 'basic', 'text', delta, source
+  console.info 'basic', 'text', delta, source
   return if source == 'api'
   advancedEditor.updateContents(delta)
   sourceDelta = basicEditor.getContents()
@@ -35,11 +35,11 @@
 )

 advancedEditor.on('selection-change', (range) ->
-  console.log 'advanced', 'selection', range
+  console.info 'advanced', 'selection', range
 )

 advancedEditor.on('text-change', (delta, source) ->
-  console.log 'advanced', 'text', delta, source
+  console.info 'advanced', 'text', delta, source
   return if source == 'api'
   basicEditor.updateContents(delta)
   sourceDelta = advancedEditor.getContents()


ignore dom changes in unit test

ignore dom changes in unit test

--- tests/unit/scripts/utils.coffee
+++ tests/unit/scripts/utils.coffee
@@ -70,6 +70,7 @@
         </div>
       '))
       editor = new Tandem.Editor('test-container')
+      editor.ignoreDomChanges = true
       return editor

     tests = [{


Fix typo

Fix typo

--- lib/init.coffee
+++ lib/init.coffee
@@ -20,6 +20,6 @@
   activate: ->
     console.log 'activate linter-package-json-validator' if atom.inDevMode()

-    packageDeps.install 'package-json-validator'
+    packageDeps.install 'linter-package-json-validator'

   provideLinter: -> PJVProvider


Add meta-e keybinding

Add meta-e keybinding

--- src/app/keymaps/command-panel.coffee
+++ src/app/keymaps/command-panel.coffee
@@ -7,4 +7,5 @@

 window.keymap.bindKeys '.editor',
   'meta-g': 'command-panel:repeat-relative-address'
+  'meta-e': 'command-panel:set-selection-as-regex-address'
   'meta-f': 'command-panel:find-in-file'


Make error dialog overridable

Make error dialog overridable

--- framer/Debug.coffee
+++ framer/Debug.coffee
@@ -81,4 +81,4 @@

 	_errorWarningLayer = layer

-window.addEventListener("error", errorWarning)
+window.error = errorWarning


Zoom inserted HTML using the devicePixelRatio

--- framer/Layer.coffee
+++ framer/Layer.coffee
@@ -529,6 +529,7 @@
 	updateForDevicePixelRatioChange: =>
 		for cssProperty in ["width", "height", "webkitTransform", "boxShadow", "textShadow", "borderRadius", "border", "fontSize", "letterSpacing", "wordSpacing", "textIndent"]
 			@_element.style[cssProperty] = LayerStyle[cssProperty](@)
+		@_elementHTML.style.zoom = @devicePixelRatio

 	##############################################################
 	# SCREEN GEOMETRY
@@ -668,6 +669,7 @@
 				@_elementHTML = document.createElement "div"
 				@_element.appendChild @_elementHTML

+			@_elementHTML.style.zoom = @devicePixelRatio
 			@_elementHTML.innerHTML = value

 			# If the contents contains something else than plain text


basic from screen to point

basic from screen to point

--- framer/Layer.coffee
+++ framer/Layer.coffee
@@ -275,9 +275,28 @@
 			y: point.y || 0
 			z: point.z || 0
 		point = @matrix3d.point(point)
+
 		for layer in @superLayers(context)
 			point.z = 0 if layer.flat
 			point = layer.matrix3d.point(point)
+			if !layer.superLayer
+				point.z = 0
+
+		point
+
+	_fromScreenPoint: (point = {}, context = false) =>
+		point =
+			x: point.x || 0
+			y: point.y || 0
+			z: point.z || 0
+		superLayers = @superLayers(context)
+		if superLayers.length
+			point = superLayers.pop().matrix3d.inverse().point(point)
+		superLayers.reverse()
+		superLayers.push(@)
+		for layer in superLayers
+			point = layer.matrix3d.inverse().point(point)
+
 		point

 	_boundingBox: (context = true) =>
@@ -387,6 +406,12 @@
 		# Convert a point on screen to this views coordinate system
 		# TODO: needs tests
 		Utils.convertPoint point, null, @
+
+	convertPointFromScreen: (point) ->
+		@_fromScreenPoint(point)
+
+	convertPointFromCanvas: (point) ->
+		@_fromScreenPoint(point, true)

 	convertPointToScreen: (point) ->
 		@_screenPoint(point)


Updated indexOfSelection method

Updated indexOfSelection method

Now accepts a parameter: the array of content to search through for selected objects (to determine indexes)

--- packages/emberella/lib/mixins/selectable_mixin.coffee
+++ packages/emberella/lib/mixins/selectable_mixin.coffee
@@ -313,14 +313,14 @@
       }

     @method indexOfSelection
+    @param content Array to search for current selection
     @return {Object|Boolean} `false` if nothing selected
   ###
-  indexOfSelection: ->
+  indexOfSelection: (content = get(@, 'arrangedContent').toArray()) ->
     result = indexes: Ember.A(), first: null, last: null
-    content = get(@, 'content').toArray()
     selection = get(@, 'selection')

-    return false if selection.length is 0
+    return false if selection.length is 0 or !Ember.isArray(content)

     for selected in selection
       idx = content.indexOf selected


Update main.coffee

Update main.coffee

修正無窮迴圈

--- lib/main.coffee
+++ lib/main.coffee
@@ -228,4 +228,4 @@
 				temp.html '尋找擴充套件'

 			return
-		), 0)
+		), 1000)


Automatically test PHP

Automatically test PHP

On activate and change of the executable path attempt to execute PHP to check that it is valid.

--- lib/main.coffee
+++ lib/main.coffee
@@ -1,4 +1,5 @@
 {CompositeDisposable} = require 'atom'
+helpers = require('atom-linter')

 module.exports =
   config:
@@ -7,18 +8,32 @@
       title: 'PHP Executable Path'
       default: 'php' # Let OS's $PATH handle the rest

+  _testBin: ->
+    title = 'linter-php: Unable to determine PHP version'
+    message = 'Unable to determine the version of "' + @executablePath +
+      '", please verify that this is the right path to PHP.'
+    try
+      helpers.exec(@executablePath, ['-v']).then (output) =>
+        regex = /PHP (\d+)\.(\d+)\.(\d+)/g
+        if not regex.exec(output)
+          atom.notifications.addError(title, {detail: message})
+          @executablePath = ''
+      .catch (e) ->
+        console.log e
+        atom.notifications.addError(title, {detail: message})
+
   activate: ->
     require('atom-package-deps').install('linter-php')
     @subscriptions = new CompositeDisposable
     @subscriptions.add atom.config.observe 'linter-php.executablePath',
       (executablePath) =>
         @executablePath = executablePath
+        @_testBin()

   deactivate: ->
     @subscriptions.dispose()

   provideLinter: ->
-    helpers = require('atom-linter')
     provider =
       name: 'PHP'
       grammarScopes: ['text.html.php', 'source.php']
@@ -27,6 +42,7 @@
       lint: (textEditor) =>
         filePath = textEditor.getPath()
         command = @executablePath
+        return [] unless command?
         parameters = []
         parameters.push('--syntax-check')
         parameters.push('--no-php-ini')