tryGet, and afterRender bugfix

tryGet, and afterRender bugfix

--- components.coffee
+++ components.coffee
@@ -378,6 +378,11 @@

         componentSpec.componentDidMount?.apply @

+        return if @_showingLoader
+        prevAfterRenderCallbacks = @_afterRenderCallbacks ? []
+        @_afterRenderCallbacks = []
+        callback() for callback in prevAfterRenderCallbacks
+

     reactSpec.shouldComponentUpdate = (nextProps, nextState) ->
         not @_elementVar? or @_elementVar.invalidated
@@ -385,7 +390,6 @@

     reactSpec.componentDidUpdate = (prevProps, prevState) ->
         return if @_showingLoader
-
         prevAfterRenderCallbacks = @_afterRenderCallbacks ? []
         @_afterRenderCallbacks = []
         callback() for callback in prevAfterRenderCallbacks
@@ -428,6 +432,17 @@
                 return true
             ancestor = ancestor._owner
         false
+
+
+    reactSpec.tryGet = (reactiveName, defaultValue) ->
+        if reactiveName not of @reactives
+            throw new Meteor.Error "Invalid reactive name: #{@}.#{reactiveName}"
+
+        J.tryGet(
+            => @reactives[reactiveName].get()
+            defaultValue
+        )
+

     reactSpec.renderLoader ?= ->


Reimplemented component reactives using AutoVar

--- components.coffee
+++ components.coffee
@@ -68,50 +68,10 @@
         if reactiveName of reactSpec
             throw new Meteor.Error "Name conflict between #{componentName}.#{reactiveName} and
                 #{componentName}.reactives.#{reactiveName}"
-        unless _.isFunction(reactiveSpec.val) or (
-            # type is J.$instanceOf(J.AutoDict) and
-            _.isFunction(reactiveSpec.keys) and
-            _.isFunction(reactiveSpec.valForKey)
-        )
+        unless _.isFunction(reactiveSpec.val)
             throw new Meteor.Error "#{componentName}.reactives.#{reactiveName} must have a val function"

-        if reactiveSpec.valForKey? then reactSpec[reactiveName] =
-            do (reactiveName, reactiveSpec) -> ->
-                @reactives[reactiveName] ?= J.AutoDict(
-                    reactiveSpec.keys.bind @
-                    reactiveSpec.valForKey.bind @
-                    reactiveSpec.onChange ? null
-                    reactiveSpec.same ? J.util.equals
-                )
-
-        else reactSpec[reactiveName] = do (reactiveName, reactiveSpec) -> ->
-            equalsFunc = reactiveSpec.same ? J.util.equals
-
-            updateReactiveVar = =>
-                oldValue = Tracker.nonreactive => @reactives[reactiveName].get()
-                newValue = reactiveSpec.val.apply @
-                if newValue is undefined
-                    throw new Meteor.Error "Can't return undefined from #{@}.reactives.#{reactiveName}.val"
-
-                @reactives[reactiveName].set newValue
-
-                if reactiveSpec.onChange? and not equalsFunc oldValue, newValue
-                    reactiveSpec.onChange.call @, oldValue, newValue
-
-            if @_reactiveComps[reactiveName]?.invalidated
-                # Accessing an invalidated reactive before Meteor's flush has
-                # time to recompute it. We'll effectively move it to the front
-                # of the flush queue so it never returns an invalidated value.
-                @_reactiveComps[reactiveName].stop()
-                @_reactiveComps[reactiveName] = Tracker.nonreactive =>
-                    Tracker.autorun (c) => updateReactiveVar()
-            else
-                valuePeek = Tracker.nonreactive => @reactives[reactiveName].get()
-                if valuePeek is undefined
-                    J.assert reactiveName not of @_reactiveComps
-                    @_reactiveComps[reactiveName] = Tracker.nonreactive =>
-                        Tracker.autorun (c) => updateReactiveVar()
-
+        reactSpec[reactiveName] = do (reactiveName) -> ->
             @reactives[reactiveName].get()

@@ -153,14 +113,13 @@
                 @_props[propName].get()

         # Set up @reactives
-        @_reactiveComps = {} # nonDictReactiveName: computation
-        @reactives = {} # reactiveName: reactiveVar|J.AutoDict
+        @reactives = {} # reactiveName: autoVar
         for reactiveName, reactiveSpec of componentSpec.reactives ? {}
-            if reactiveSpec.val?
-                # AutoDicts get initialized later because they
-                # immediately want to run their keyFunc.
-                @reactives[reactiveName] = new ReactiveVar undefined,
-                    reactiveSpec.same ? J.util.equals
+            @reactives[reactiveName] = J.AutoVar(
+                reactiveSpec.val.bind @
+                reactiveSpec.onChange ? null
+                reactiveSpec.same ? J.util.equals
+            )

         # Set up @state
         initialState = {}
@@ -257,32 +216,13 @@
             @_renderComp.stop()
             @_renderComp = null

-        for reactiveName of @reactives
-            if reactiveName of @_reactiveComps
-                # It's a ReactiveVar
-                val = Tracker.nonreactive => @reactives[reactiveName].get()
-                if val instanceof J.AutoDict or val instanceof J.AutoList or val instanceof J.AutoVar
-                    val.stop()
-                @_reactiveComps[reactiveName].stop()
-            else
-                # It's an AutoDict
-                @reactives[reactiveName].stop()
-        @_reactiveComps = null
-        @reactives = null
+        for reactiveName, autoVar of @reactives
+            autoVar.stop()

         componentSpec.componentWillUnmount?.apply @

     reactSpec.render = ->
-        # Make sure we've run all the reactives' val-functions at least once.
-        # Even if the render function never uses a reactive, that reactive
-        # might need to monitor a value and do some side effect in its onChange.
-        for reactiveName, reactiveSpec of componentSpec.reactives
-            # We should be able to optimize this by only running
-            # reactives with onChange functions, since val functions
-            # aren't supposed to have side effects, but whatever.
-            @[reactiveName]()
-
         ###
             There are three times at which a component may be re-rendered:

@@ -322,20 +262,21 @@
             renderedComponent = componentSpec.render.apply @

         @_renderComp.onInvalidate (c) =>
-            unless c.stopped
-                @_renderComp.stop()
-
-                Tracker.afterFlush =>
-                    # The component may have unmounted
-                    return unless @isMounted()
-
-                    # Between c's invalidation time and now, the component may
-                    # have been re-rendered at time (1) - see comment above - in
-                    # which case there would be a new @_renderComp
-                    return unless c is @_renderComp
-
-                    @_renderComp = null
-                    @forceUpdate()
+            return if c.stopped
+
+            @_renderComp.stop()
+
+            Tracker.afterFlush =>
+                # The component may have unmounted
+                return unless @isMounted()
+
+                # Between c's invalidation time and now, the component may
+                # have been re-rendered at time (1) - see comment above - in
+                # which case there would be a new @_renderComp
+                return unless c is @_renderComp
+
+                @_renderComp = null
+                @forceUpdate()

         renderedComponent


Fixed a bug in the DOMInflator which made it fail in Firefox

--- src/spine/modules/dom_inflator.coffee
+++ src/spine/modules/dom_inflator.coffee
@@ -4,6 +4,7 @@
     ((@filters ||= {})['before_prepare'] ||= []).push 'inflateFromDOM'

   inflateFromDOM: ->
+    console.log 'inflateFromDOM!'
     return unless typeof @['deactivateAndKillOrphans'] == 'function'

     classNames = []
@@ -89,7 +90,7 @@
     attributes = {id: id}
     for attr in modelClass.attributes
       if targetEl = el.find("[data-#{className}-attribute='#{attr}']")[0]
-        attributes[attr] = targetEl.innerText
+        attributes[attr] = $(targetEl).text()

     model = new modelClass(attributes)
     # Insert the new model into the Spine.Model Collection


Re-ordered when flush should occur.

Re-ordered when flush should occur.

--- src/ItemBuffer.coffee
+++ src/ItemBuffer.coffee
@@ -9,9 +9,9 @@

   add: (id, item) ->
     if @_buffer[id]? then Logger.warn('Replacing ItemBuffer item', id, @_buffer[id], item)
-    if @_size + 1 > @_maxSize then @flush()
     @_buffer[id] = item
     @_size++
+    if @_size > @_maxSize then @flush()

   remove: (id) ->
     return unless @_buffer[id]?


Add arrow keys to key-utils.coffee

Add arrow keys to key-utils.coffee

--- extension/packages/key-utils.coffee
+++ extension/packages/key-utils.coffee
@@ -24,6 +24,10 @@
       [ KE.DOM_VK_SPACE,          KE.DOM_VK_SPACE,      'Space',     'Shift-Space'      ],
       [ KE.DOM_VK_TAB,            KE.DOM_VK_TAB,        'Tab',       'Shift-Tab'        ],
       [ KE.DOM_VK_RETURN,         KE.DOM_VK_RETURN,     'Return',    'Shift-Return'     ],
+      [ KE.DOM_VK_LEFT,           KE.DOM_VK_LEFT,       'Left',      'Shift-Left'       ],
+      [ KE.DOM_VK_RIGHT,          KE.DOM_VK_RIGHT,      'Right',     'Shift-Right'      ],
+      [ KE.DOM_VK_UP,             KE.DOM_VK_UP,         'Up',        'Shift-Up'         ],
+      [ KE.DOM_VK_DOWN,           KE.DOM_VK_DOWN,       'Down',      'Shift-Down'       ],

       [ KE.DOM_VK_1,              KE.DOM_VK_EXCLAMATION,          '1',  '!' ],
       [ KE.DOM_VK_2,              KE.DOM_VK_AT,                   '2',  '@' ],


ﾌﾟｼｭﾌﾟｼｭ

ﾌﾟｼｭﾌﾟｼｭ

--- src/coffee/kazitori.coffee
+++ src/coffee/kazitori.coffee
@@ -183,8 +183,8 @@
 		###

 		url = @.root + frag.replace(routeStripper, '')
-
-		if @_matchCheck(@.fragment) is false
+		matched = @_matchCheck(@.fragment, @.handlers)
+		if matched is false
 			if @.notFound isnt null
 				@change(@.notFound)
 			@._dispatcher.dispatchEvent(new KazitoriEvent(KazitoriEvent.NOT_FOUND))
@@ -201,7 +201,7 @@
 			return @.location.assign(url)
 		#イベントディスパッチ
 		@dispatchEvent(new KazitoriEvent(KazitoriEvent.CHANGE, next, prev))
-		@loadURL(frag)
+		@loadURL(frag, matched)
 		return

 	#中断する
@@ -231,10 +231,11 @@
 		return @

 	#URL を読み込む
-	loadURL:(fragmentOverride)->
+	loadURL:(fragmentOverride, matched)->
 		fragment = @.fragment = @getFragment(fragmentOverride)
 		matched = []
-
+		beforesMatched = []
+
 		if @.beforeAnytimeHandler or @.beforeHandlers.length > 0
 			@._beforeDeffer = new Deffered()
 			@._beforeDeffer.queue = []
@@ -405,13 +406,12 @@
 	# ここでここまでのチェックを実際に行うなら
 	# loadURL, executeHandler 内で同じチェックは行う必要がないはずなので
 	# それぞれのメソッドが簡潔になるようにリファクタする必要がある
-	_matchCheck:(fragment)->
+	_matchCheck:(fragment, handlers)->
 		matched = []
-		for handler in @.handlers
+		for handler in handlers
 			if handler.rule is fragment
-				matched.push true
-
-			if handler.test(fragment)
+				matched.push handler
+			else if handler.test(fragment)
 				if handler.isVariable and handler.types.length > 0
 					#型チェック用
 					args = handler._extractParams(fragment)
@@ -426,10 +426,11 @@
 							argsMatch.push true
 						i++
 					if not false in argsMatch
-						matched.push true
+						matched.push handler
 				else
-					matched.push true
-		return if true in matched then true else false
+					matched.push handler
+		return if matched.length > 0 then matched else false
+		# return if true in matched then true else false


Use single quotes.

Use single quotes.

--- src/coffee/services.coffee
+++ src/coffee/services.coffee
@@ -35,75 +35,75 @@
   mapOrder: (order) ->
     @debug("mapOrder: #{order.id}") if order.id

-    xml = builder.create("order", { "version": "1.0", "encoding": "UTF-8", "standalone": true })
-    xml.e("xsdVersion").t("0.8")
+    xml = builder.create('order', { 'version': '1.0', 'encoding': 'UTF-8', 'standalone': true })
+    xml.e('xsdVersion').t('0.8')

-    xml.e("externalCustomerId").t("UNKNOWN").up()
+    xml.e('externalCustomerId').t('UNKNOWN').up()

-    attribs = [ "id", "version", "createdAt", "lastModifiedAt", "customerId", "customerEmail",
-                "country", "orderState", "shipmentState", "paymentState" ]
+    attribs = [ 'id', 'version', 'createdAt', 'lastModifiedAt', 'customerId', 'customerEmail',
+                'country', 'orderState', 'shipmentState', 'paymentState' ]

     for attr in attribs
       @add(xml, order, attr)

     if order.taxedPrice
       price = order.taxedPrice
-      xPrice = xml.e("taxedPrice")
-      @money(xPrice, price, "totalNet")
-      @money(xPrice, price, "totalGross")
+      xPrice = xml.e('taxedPrice')
+      @money(xPrice, price, 'totalNet')
+      @money(xPrice, price, 'totalGross')

       for tax in price.taxPortions
-        xT = xPrice.e("taxPortions")
-        xT.e("rate").t(tax.rate)
-        @money(xT, tax, "amount")
+        xT = xPrice.e('taxPortions')
+        xT.e('rate').t(tax.rate)
+        @money(xT, tax, 'amount')

     if order.shippingAddress
-      @mapAddress(xml.e("shippingAddress"), order.shippingAddress)
+      @mapAddress(xml.e('shippingAddress'), order.shippingAddress)

     if order.billingAddress
-      @mapAddress(xml.e("billingAddress"), order.billingAddress)
+      @mapAddress(xml.e('billingAddress'), order.billingAddress)

     @customerGroup(xml, order)

     if order.paymentInfo
       pi = order.paymentInfo
-      xPi = xml.e("paymentInfo")
-      @add(xPi, pi, "paymentMethod")
-      @add(xPi, pi, "paymentID")
+      xPi = xml.e('paymentInfo')
+      @add(xPi, pi, 'paymentMethod')
+      @add(xPi, pi, 'paymentID')

     if order.shippingInfo
       si = order.shippingInfo
-      xSi = xml.e("shippingInfo")
-      @add(xSi, si, "shippingMethodName")
-      @add(xSi, si, "trackingData")
+      xSi = xml.e('shippingInfo')
+      @add(xSi, si, 'shippingMethodName')
+      @add(xSi, si, 'trackingData')

-      @money(xSi, si, "price")
+      @money(xSi, si, 'price')
       @taxRate(xSi, si)

     if order.lineItems
       for lineItem in order.lineItems
-        xLi = xml.e("lineItems")
-        @add(xLi, lineItem, "id")
-        @add(xLi, lineItem, "productId")
-        @add(xLi, lineItem.name, "de", "name")
+        xLi = xml.e('lineItems')
+        @add(xLi, lineItem, 'id')
+        @add(xLi, lineItem, 'productId')
+        @add(xLi, lineItem.name, 'de', 'name')

         variant = lineItem.variant
-        xVariant = xLi.e("variant")
-        @add(xVariant, variant, "id")
-        @add(xVariant, variant, "sku")
+        xVariant = xLi.e('variant')
+        @add(xVariant, variant, 'id')
+        @add(xVariant, variant, 'sku')
         if variant.prices
           for price in variant.prices
-            @priceElem(xVariant.e("prices"), price)
+            @priceElem(xVariant.e('prices'), price)

         if variant.attributes
           for attr in variant.attributes
-            @attributes(xVariant.e("attributes"), attr)
+            @attributes(xVariant.e('attributes'), attr)

         # Adds sku again directly to the lineItem
-        @add(xLi, variant, "sku")
+        @add(xLi, variant, 'sku')

         @price(xLi, lineItem)
-        @add(xLi, lineItem, "quantity")
+        @add(xLi, lineItem, 'quantity')

         # Adds lineItem price
         @lineItemPrice xLi, lineItem.price.value.centAmount, lineItem.quantity, lineItem.price.value.currencyCode
@@ -112,34 +112,34 @@

     if order.customLineItems
       for customLineItem in order.customLineItems
-        @customLineItem(xml.e("customLineItems"), customLineItem)
+        @customLineItem(xml.e('customLineItems'), customLineItem)

-    xml.end(pretty: true, indent: "  ", newline: "\n")
+    xml.end(pretty: true, indent: '  ', newline: "\n")

   customLineItem: (xml, elem) ->
-    @add(xml, elem, "id")
-    @add(xml, elem.name, "de", "name")
-    @money(xml, elem, "money")
-    @add(xml, elem, "slug")
-    @add(xml, elem, "quantity")
+    @add(xml, elem, 'id')
+    @add(xml, elem.name, 'de', 'name')
+    @money(xml, elem, 'money')
+    @add(xml, elem, 'slug')
+    @add(xml, elem, 'quantity')
     @lineItemPrice xml, elem.money.centAmount, elem.quantity, elem.money.currencyCode
     @taxRate(xml, elem)

   attributes: (xml, elem) ->
-    xml.e("name").t(elem.name).up()
-      .e("value").t(elem.value)
+    xml.e('name').t(elem.name).up()
+      .e('value').t(elem.value)

   money: (xml, elem, name) ->
     xml.e(name)
-      .e("currencyCode").t(elem[name].currencyCode).up()
-      .e("centAmount").t(elem[name].centAmount)
+      .e('currencyCode').t(elem[name].currencyCode).up()
+      .e('centAmount').t(elem[name].centAmount)

-  price: (xml, elem, name="price") ->
+  price: (xml, elem, name='price') ->
     @priceElem(xml.e(name), elem.price)

   priceElem: (xP, p) ->
-    @money(xP, p, "value")
-    @add(xP, p, "country")
+    @money(xP, p, 'value')
+    @add(xP, p, 'country')
     @customerGroup(xP, p)

   lineItemPrice: (xml, centAmount, quantity, currencyCode) ->
@@ -149,27 +149,27 @@
         value:
           currencyCode: currencyCode
           centAmount: total
-    @price(xml, p, "lineItemPrice")
+    @price(xml, p, 'lineItemPrice')

   taxRate: (xml, elem) ->
     tr = elem.taxRate
-    xTr = xml.e("taxRate")
-    attribs = [ "id", "name", "amount", "includedInPrice", "country", "state" ]
+    xTr = xml.e('taxRate')
+    attribs = [ 'id', 'name', 'amount', 'includedInPrice', 'country', 'state' ]
     for attr in attribs
       @add(xTr, tr, attr)

   customerGroup: (xml, elem) ->
     cg = elem.customerGroup
     if cg
-      xCg = xml.e("customerGroup")
-      @add(xCg, cg, "id")
+      xCg = xml.e('customerGroup')
+      @add(xCg, cg, 'id')
       if cg.obj
-        @add(xCg, cg.obj, "version")
-        @add(xCg, cg.obj, "name")
+        @add(xCg, cg.obj, 'version')
+        @add(xCg, cg.obj, 'name')

   mapAddress: (xml, address) ->
-    attribs = [ "id", "title", "salutation", "firstName", "lastName", "streetName", "streetNumber", "additionalStreetInfo", "postalCode",
-                "city", "region", "state", "country", "company", "department", "building", "apartment", "pOBox", "phone", "mobile", "email" ]
+    attribs = [ 'id', 'title', 'salutation', 'firstName', 'lastName', 'streetName', 'streetNumber', 'additionalStreetInfo', 'postalCode',
+                'city', 'region', 'state', 'country', 'company', 'department', 'building', 'apartment', 'pOBox', 'phone', 'mobile', 'email' ]
     for attr in attribs
       @add(xml, address, attr)


Update turbolinks_slide.js.coffee

Update turbolinks_slide.js.coffee

added a type "show"

--- turbolinks_slide.js.coffee
+++ turbolinks_slide.js.coffee
@@ -21,7 +21,7 @@
         when 'index'
           return 1 if (@current.action != 'index')
           break
-        when 'new', 'edit'
+        when 'new', 'edit', 'show'
           return -1 if (@current.action == 'index')
           break
         else


Move LocalHints out of link-hints mode.

Move LocalHints out of link-hints mode.

This code (LocalHints) has been embedded into the middle of the
link-hints mode class.  And it shouldn't be.

This moves it out, and allows us to unwind some of the gymnastics #2048
(global link hints) introduced to avoid having an incomprehensible diff.

--- content_scripts/link_hints.coffee
+++ content_scripts/link_hints.coffee
@@ -101,7 +101,7 @@
   activateModeToOpenIncognito: (count) -> @activateMode count, OPEN_INCOGNITO
   activateModeToDownloadLink: (count) -> @activateMode count, DOWNLOAD_LINK_URL

-class LinkHintsModeBase # This is temporary, because the "visible hints" code is embedded in the hints class.
+class LinkHintsMode
   hintMarkerContainingDiv: null
   # One of the enums listed at the top of this file.
   mode: undefined
@@ -177,8 +177,310 @@

       marker

-# TODO(smblott)  This is temporary.  Unfortunately, this code is embedded in the "old" link-hints mode class.
-# It should be moved, but it's left here for the moment to help keep the diff clearer.
+  # Handles <Shift> and <Ctrl>.
+  onKeyDownInMode: (hintMarkers, event) ->
+    return if event.repeat
+    @keydownKeyChar = KeyboardUtils.getKeyChar(event).toLowerCase()
+
+    previousTabCount = @tabCount
+    @tabCount = 0
+
+    if event.keyCode in [ keyCodes.shiftKey, keyCodes.ctrlKey ] and
+      @mode in [ OPEN_IN_CURRENT_TAB, OPEN_WITH_QUEUE, OPEN_IN_NEW_BG_TAB, OPEN_IN_NEW_FG_TAB ]
+        @tabCount = previousTabCount
+        # Toggle whether to open the link in a new or current tab.
+        previousMode = @mode
+        keyCode = event.keyCode
+
+        switch keyCode
+          when keyCodes.shiftKey
+            @setOpenLinkMode(if @mode is OPEN_IN_CURRENT_TAB then OPEN_IN_NEW_BG_TAB else OPEN_IN_CURRENT_TAB)
+          when keyCodes.ctrlKey
+            @setOpenLinkMode(if @mode is OPEN_IN_NEW_FG_TAB then OPEN_IN_NEW_BG_TAB else OPEN_IN_NEW_FG_TAB)
+
+        handlerId = handlerStack.push
+          keyup: (event) =>
+            if event.keyCode == keyCode
+              handlerStack.remove()
+              @setOpenLinkMode previousMode
+            true # Continue bubbling the event.
+
+        # For some (unknown) reason, we don't always receive the keyup event needed to remove this handler.
+        # Therefore, we ensure that it's always removed when hint mode exits.  See #1911 and #1926.
+        @hintMode.onExit -> handlerStack.remove handlerId
+
+    else if event.keyCode in [ keyCodes.backspace, keyCodes.deleteKey ]
+      if @markerMatcher.popKeyChar()
+        @updateVisibleMarkers hintMarkers
+      else
+        # Exit via @hintMode.exit(), so that the LinkHints.activate() "onExit" callback sees the key event and
+        # knows not to restart hints mode.
+        @hintMode.exit event
+
+    else if event.keyCode == keyCodes.enter
+      # Activate the active hint, if there is one.  Only FilterHints uses an active hint.
+      HintCoordinator.sendMessage "activateActiveHintMarker" if @markerMatcher.activeHintMarker
+
+    else if event.keyCode == keyCodes.tab
+      @tabCount = previousTabCount + (if event.shiftKey then -1 else 1)
+      @updateVisibleMarkers hintMarkers, @tabCount
+
+    else
+      return
+
+    # We've handled the event, so suppress it.
+    DomUtils.suppressEvent event
+
+  # Handles normal input.
+  onKeyPressInMode: (hintMarkers, event) ->
+    return if event.repeat
+
+    keyChar = String.fromCharCode(event.charCode).toLowerCase()
+    if keyChar
+      @markerMatcher.pushKeyChar keyChar, @keydownKeyChar
+      @updateVisibleMarkers hintMarkers
+
+    # We've handled the event, so suppress it.
+    DomUtils.suppressEvent event
+
+  updateVisibleMarkers: (hintMarkers, tabCount = 0) ->
+    {hintKeystrokeQueue, linkTextKeystrokeQueue} = @markerMatcher
+    HintCoordinator.sendMessage "updateKeyState", {hintKeystrokeQueue, linkTextKeystrokeQueue, tabCount}
+
+  updateKeyState: (hintMarkers, {hintKeystrokeQueue, linkTextKeystrokeQueue, tabCount}) ->
+    extend @markerMatcher, {hintKeystrokeQueue, linkTextKeystrokeQueue}
+
+    {linksMatched, userMightOverType} = @markerMatcher.getMatchingHints hintMarkers, tabCount
+    if linksMatched.length == 0
+      @deactivateMode()
+    else if linksMatched.length == 1
+      @activateLink linksMatched[0], userMightOverType ? false
+    else
+      @hideMarker marker for marker in hintMarkers
+      @showMarker matched, @markerMatcher.hintKeystrokeQueue.length for matched in linksMatched
+
+  # When only one hint remains, activate it in the appropriate way.  The current frame may or may not contain
+  # the matched link, and may or may not have the focus.  The resulting four cases are accounted for here by
+  # selectively pushing the appropriate HintCoordinator.onExit handlers.
+  activateLink: (linkMatched, userMightOverType=false) ->
+    @removeHintMarkers()
+    clickEl = HintCoordinator.getLocalHintMarker(linkMatched.hintDescriptor)?.element
+
+    if clickEl?
+      HintCoordinator.onExit.push =>
+        if clickEl == document.body
+          Utils.nextTick -> focusThisFrame highlight: true
+        else if DomUtils.isSelectable clickEl
+          window.focus()
+          DomUtils.simulateSelect clickEl
+        else
+          clickActivator = (modifiers) -> (link) -> DomUtils.simulateClick link, modifiers
+          linkActivator = @mode.linkActivator ? clickActivator @mode.clickModifiers
+          # TODO: Are there any other input elements which should not receive focus?
+          if clickEl.nodeName.toLowerCase() == "input" and clickEl.type not in ["button", "submit"]
+            clickEl.focus()
+          linkActivator clickEl
+
+    installKeyBoardBlocker = (startKeyboardBlocker) ->
+      if linkMatched.hintDescriptor.frameId == frameId
+        flashEl = DomUtils.addFlashRect linkMatched.hintDescriptor.rect
+        HintCoordinator.onExit.push -> DomUtils.removeElement flashEl
+
+      if document.hasFocus()
+        startKeyboardBlocker -> HintCoordinator.sendMessage "exit"
+
+    HintCoordinator.onExit.push => @deactivateMode()
+    # If we're using a keyboard blocker, then the frame with the focus sends the "exit" message, otherwise the
+    # frame containing the matched link does.
+    if userMightOverType and Settings.get "waitForEnterForFilteredHints"
+      installKeyBoardBlocker (callback) -> new WaitForEnter callback
+    else if userMightOverType
+      installKeyBoardBlocker (callback) -> new TypingProtector 200, callback
+    else if linkMatched.hintDescriptor.frameId == frameId
+      DomUtils.flashRect linkMatched.hintDescriptor.rect
+      HintCoordinator.sendMessage "exit"
+
+  #
+  # Shows the marker, highlighting matchingCharCount characters.
+  #
+  showMarker: (linkMarker, matchingCharCount) ->
+    return unless linkMarker.hintDescriptor.frameId == frameId
+    linkMarker.style.display = ""
+    for j in [0...linkMarker.childNodes.length]
+      if (j < matchingCharCount)
+        linkMarker.childNodes[j].classList.add("matchingCharacter")
+      else
+        linkMarker.childNodes[j].classList.remove("matchingCharacter")
+
+  hideMarker: (linkMarker) -> linkMarker.style.display = "none"
+
+  deactivateMode: ->
+    @removeHintMarkers()
+    @hintMode?.exit()
+
+  removeHintMarkers: ->
+    DomUtils.removeElement @hintMarkerContainingDiv if @hintMarkerContainingDiv
+    @hintMarkerContainingDiv = null
+
+# Use characters for hints, and do not filter links by their text.
+class AlphabetHints
+  constructor: ->
+    @linkHintCharacters = Settings.get "linkHintCharacters"
+    # We use the keyChar from keydown if the link-hint characters are all "a-z0-9".  This is the default
+    # settings value, and preserves the legacy behavior (which always used keydown) for users which are
+    # familiar with that behavior.  Otherwise, we use keyChar from keypress, which admits non-Latin
+    # characters. See #1722.
+    @useKeydown = /^[a-z0-9]*$/.test @linkHintCharacters
+    @hintKeystrokeQueue = []
+
+  fillInMarkers: (hintMarkers) ->
+    hintStrings = @hintStrings(hintMarkers.length)
+    for marker, idx in hintMarkers
+      marker.hintString = hintStrings[idx]
+      marker.innerHTML = spanWrap(marker.hintString.toUpperCase())
+
+    hintMarkers
+
+  #
+  # Returns a list of hint strings which will uniquely identify the given number of links. The hint strings
+  # may be of different lengths.
+  #
+  hintStrings: (linkCount) ->
+    hints = [""]
+    offset = 0
+    while hints.length - offset < linkCount or hints.length == 1
+      hint = hints[offset++]
+      hints.push ch + hint for ch in @linkHintCharacters
+    hints = hints[offset...offset+linkCount]
+
+    # Shuffle the hints so that they're scattered; hints starting with the same character and short hints are
+    # spread evenly throughout the array.
+    return hints.sort().map (str) -> str.reverse()
+
+  getMatchingHints: (hintMarkers) ->
+    matchString = @hintKeystrokeQueue.join ""
+    linksMatched: hintMarkers.filter (linkMarker) -> linkMarker.hintString.startsWith matchString
+
+  pushKeyChar: (keyChar, keydownKeyChar) ->
+    @hintKeystrokeQueue.push (if @useKeydown then keydownKeyChar else keyChar)
+  popKeyChar: -> @hintKeystrokeQueue.pop()
+
+# Use numbers (usually) for hints, and also filter links by their text.
+class FilterHints
+  constructor: ->
+    @linkHintNumbers = Settings.get "linkHintNumbers"
+    @hintKeystrokeQueue = []
+    @linkTextKeystrokeQueue = []
+    @activeHintMarker = null
+    # The regexp for splitting typed text and link texts.  We split on sequences of non-word characters and
+    # link-hint numbers.
+    @splitRegexp = new RegExp "[\\W#{Utils.escapeRegexSpecialCharacters @linkHintNumbers}]+"
+
+  generateHintString: (linkHintNumber) ->
+    base = @linkHintNumbers.length
+    hint = []
+    while 0 < linkHintNumber
+      hint.push @linkHintNumbers[Math.floor linkHintNumber % base]
+      linkHintNumber = Math.floor linkHintNumber / base
+    hint.reverse().join ""
+
+  renderMarker: (marker) ->
+    marker.innerHTML = spanWrap(marker.hintString +
+        (if marker.showLinkText then ": " + marker.linkText else ""))
+
+  fillInMarkers: (hintMarkers) ->
+    @renderMarker marker for marker in hintMarkers
+
+    # We use @filterLinkHints() here (although we know that all of the hints will match) to fill in the hint
+    # strings.  This ensures that we always get hint strings in the same order.
+    @filterLinkHints hintMarkers
+
+  getMatchingHints: (hintMarkers, tabCount = 0) ->
+    # At this point, linkTextKeystrokeQueue and hintKeystrokeQueue have been updated to reflect the latest
+    # input. use them to filter the link hints accordingly.
+    matchString = @hintKeystrokeQueue.join ""
+    linksMatched = @filterLinkHints hintMarkers
+    linksMatched = linksMatched.filter (linkMarker) -> linkMarker.hintString.startsWith matchString
+
+    # Visually highlight of the active hint (that is, the one that will be activated if the user
+    # types <Enter>).
+    tabCount = ((linksMatched.length * Math.abs tabCount) + tabCount) % linksMatched.length
+    @activeHintMarker?.classList.remove "vimiumActiveHintMarker"
+    @activeHintMarker = linksMatched[tabCount]
+    @activeHintMarker?.classList.add "vimiumActiveHintMarker"
+
+    linksMatched: linksMatched
+    userMightOverType: @hintKeystrokeQueue.length == 0 and 0 < @linkTextKeystrokeQueue.length
+
+  pushKeyChar: (keyChar, keydownKeyChar) ->
+    # For filtered hints, we *always* use the keyChar value from keypress, because there is no obvious and
+    # easy-to-understand meaning for choosing one of keyChar or keydownKeyChar (as there is for alphabet
+    # hints).
+    if 0 <= @linkHintNumbers.indexOf keyChar
+      @hintKeystrokeQueue.push keyChar
+    else
+      # Since we might renumber the hints, we should reset the current hintKeyStrokeQueue.
+      @hintKeystrokeQueue = []
+      @linkTextKeystrokeQueue.push keyChar
+
+  popKeyChar: ->
+    @hintKeystrokeQueue.pop() or @linkTextKeystrokeQueue.pop()
+
+  # Filter link hints by search string, renumbering the hints as necessary.
+  filterLinkHints: (hintMarkers) ->
+    linkSearchString = @linkTextKeystrokeQueue.join("").trim().toLowerCase()
+    do (scoreFunction = @scoreLinkHint linkSearchString) ->
+      linkMarker.score = scoreFunction linkMarker for linkMarker in hintMarkers
+    hintMarkers = hintMarkers[..].sort (a,b) ->
+      if b.score == a.score then b.stableSortCount - a.stableSortCount else b.score - a.score
+
+    linkHintNumber = 1
+    for linkMarker in hintMarkers
+      continue unless 0 < linkMarker.score
+      linkMarker.hintString = @generateHintString linkHintNumber++
+      @renderMarker linkMarker
+      linkMarker
+
+  # Assign a score to a filter match (higher is better).  We assign a higher score for matches at the start of
+  # a word, and a considerably higher score still for matches which are whole words.
+  scoreLinkHint: (linkSearchString) ->
+    searchWords = linkSearchString.trim().split @splitRegexp
+    (linkMarker) =>
+      text = linkMarker.linkText.trim()
+      linkWords = linkMarker.linkWords ?= text.toLowerCase().split @splitRegexp
+
+      searchWordScores =
+        for searchWord in searchWords
+          linkWordScores =
+            for linkWord, idx in linkWords
+              if linkWord == searchWord
+                if idx == 0 then 8 else 6
+              else if linkWord.startsWith searchWord
+                if idx == 0 then 4 else 2
+              else if 0 <= linkWord.indexOf searchWord
+                1
+              else
+                0
+          Math.max linkWordScores...
+
+      if 0 in searchWordScores
+        0
+      else
+        addFunc = (a,b) -> a + b
+        score = searchWordScores.reduce addFunc, 0
+        # Prefer matches in shorter texts.  To keep things balanced for links without any text, we just weight
+        # them as if their length was 50.
+        score / Math.log 1 + (text.length || 50)
+
+#
+# Make each hint character a span, so that we can highlight the typed characters as you type them.
+#
+spanWrap = (hintString) ->
+  innerHTML = []
+  for char in hintString
+    innerHTML.push("" + char + "")
+  innerHTML.join("")
+
 LocalHints =
   #
   # Determine whether the element is visible and clickable. If it is, find the rect bounding the element in
@@ -405,314 +707,6 @@

     {linkText, showLinkText}

-# TODO(smblott) Again, this is temporary.  We need to move the code above out of the "old" link-hints class.
-class LinkHintsMode extends LinkHintsModeBase
-  constructor: (args...) -> super args...
-
-  # Handles <Shift> and <Ctrl>.
-  onKeyDownInMode: (hintMarkers, event) ->
-    return if event.repeat
-    @keydownKeyChar = KeyboardUtils.getKeyChar(event).toLowerCase()
-
-    previousTabCount = @tabCount
-    @tabCount = 0
-
-    if event.keyCode in [ keyCodes.shiftKey, keyCodes.ctrlKey ] and
-      @mode in [ OPEN_IN_CURRENT_TAB, OPEN_WITH_QUEUE, OPEN_IN_NEW_BG_TAB, OPEN_IN_NEW_FG_TAB ]
-        @tabCount = previousTabCount
-        # Toggle whether to open the link in a new or current tab.
-        previousMode = @mode
-        keyCode = event.keyCode
-
-        switch keyCode
-          when keyCodes.shiftKey
-            @setOpenLinkMode(if @mode is OPEN_IN_CURRENT_TAB then OPEN_IN_NEW_BG_TAB else OPEN_IN_CURRENT_TAB)
-          when keyCodes.ctrlKey
-            @setOpenLinkMode(if @mode is OPEN_IN_NEW_FG_TAB then OPEN_IN_NEW_BG_TAB else OPEN_IN_NEW_FG_TAB)
-
-        handlerId = handlerStack.push
-          keyup: (event) =>
-            if event.keyCode == keyCode
-              handlerStack.remove()
-              @setOpenLinkMode previousMode
-            true # Continue bubbling the event.
-
-        # For some (unknown) reason, we don't always receive the keyup event needed to remove this handler.
-        # Therefore, we ensure that it's always removed when hint mode exits.  See #1911 and #1926.
-        @hintMode.onExit -> handlerStack.remove handlerId
-
-    else if event.keyCode in [ keyCodes.backspace, keyCodes.deleteKey ]
-      if @markerMatcher.popKeyChar()
-        @updateVisibleMarkers hintMarkers
-      else
-        # Exit via @hintMode.exit(), so that the LinkHints.activate() "onExit" callback sees the key event and
-        # knows not to restart hints mode.
-        @hintMode.exit event
-
-    else if event.keyCode == keyCodes.enter
-      # Activate the active hint, if there is one.  Only FilterHints uses an active hint.
-      HintCoordinator.sendMessage "activateActiveHintMarker" if @markerMatcher.activeHintMarker
-
-    else if event.keyCode == keyCodes.tab
-      @tabCount = previousTabCount + (if event.shiftKey then -1 else 1)
-      @updateVisibleMarkers hintMarkers, @tabCount
-
-    else
-      return
-
-    # We've handled the event, so suppress it.
-    DomUtils.suppressEvent event
-
-  # Handles normal input.
-  onKeyPressInMode: (hintMarkers, event) ->
-    return if event.repeat
-
-    keyChar = String.fromCharCode(event.charCode).toLowerCase()
-    if keyChar
-      @markerMatcher.pushKeyChar keyChar, @keydownKeyChar
-      @updateVisibleMarkers hintMarkers
-
-    # We've handled the event, so suppress it.
-    DomUtils.suppressEvent event
-
-  updateVisibleMarkers: (hintMarkers, tabCount = 0) ->
-    {hintKeystrokeQueue, linkTextKeystrokeQueue} = @markerMatcher
-    HintCoordinator.sendMessage "updateKeyState", {hintKeystrokeQueue, linkTextKeystrokeQueue, tabCount}
-
-  updateKeyState: (hintMarkers, {hintKeystrokeQueue, linkTextKeystrokeQueue, tabCount}) ->
-    extend @markerMatcher, {hintKeystrokeQueue, linkTextKeystrokeQueue}
-
-    {linksMatched, userMightOverType} = @markerMatcher.getMatchingHints hintMarkers, tabCount
-    if linksMatched.length == 0
-      @deactivateMode()
-    else if linksMatched.length == 1
-      @activateLink linksMatched[0], userMightOverType ? false
-    else
-      @hideMarker marker for marker in hintMarkers
-      @showMarker matched, @markerMatcher.hintKeystrokeQueue.length for matched in linksMatched
-
-  # When only one hint remains, activate it in the appropriate way.  The current frame may or may not contain
-  # the matched link, and may or may not have the focus.  The resulting four cases are accounted for here by
-  # selectively pushing the appropriate HintCoordinator.onExit handlers.
-  activateLink: (linkMatched, userMightOverType=false) ->
-    @removeHintMarkers()
-    clickEl = HintCoordinator.getLocalHintMarker(linkMatched.hintDescriptor)?.element
-
-    if clickEl?
-      HintCoordinator.onExit.push =>
-        if clickEl == document.body
-          Utils.nextTick -> focusThisFrame highlight: true
-        else if DomUtils.isSelectable clickEl
-          window.focus()
-          DomUtils.simulateSelect clickEl
-        else
-          clickActivator = (modifiers) -> (link) -> DomUtils.simulateClick link, modifiers
-          linkActivator = @mode.linkActivator ? clickActivator @mode.clickModifiers
-          # TODO: Are there any other input elements which should not receive focus?
-          if clickEl.nodeName.toLowerCase() == "input" and clickEl.type not in ["button", "submit"]
-            clickEl.focus()
-          linkActivator clickEl
-
-    installKeyBoardBlocker = (startKeyboardBlocker) ->
-      if linkMatched.hintDescriptor.frameId == frameId
-        flashEl = DomUtils.addFlashRect linkMatched.hintDescriptor.rect
-        HintCoordinator.onExit.push -> DomUtils.removeElement flashEl
-
-      if document.hasFocus()
-        startKeyboardBlocker -> HintCoordinator.sendMessage "exit"
-
-    HintCoordinator.onExit.push => @deactivateMode()
-    # If we're using a keyboard blocker, then the frame with the focus sends the "exit" message, otherwise the
-    # frame containing the matched link does.
-    if userMightOverType and Settings.get "waitForEnterForFilteredHints"
-      installKeyBoardBlocker (callback) -> new WaitForEnter callback
-    else if userMightOverType
-      installKeyBoardBlocker (callback) -> new TypingProtector 200, callback
-    else if linkMatched.hintDescriptor.frameId == frameId
-      DomUtils.flashRect linkMatched.hintDescriptor.rect
-      HintCoordinator.sendMessage "exit"
-
-  #
-  # Shows the marker, highlighting matchingCharCount characters.
-  #
-  showMarker: (linkMarker, matchingCharCount) ->
-    return unless linkMarker.hintDescriptor.frameId == frameId
-    linkMarker.style.display = ""
-    for j in [0...linkMarker.childNodes.length]
-      if (j < matchingCharCount)
-        linkMarker.childNodes[j].classList.add("matchingCharacter")
-      else
-        linkMarker.childNodes[j].classList.remove("matchingCharacter")
-
-  hideMarker: (linkMarker) -> linkMarker.style.display = "none"
-
-  deactivateMode: ->
-    @removeHintMarkers()
-    @hintMode?.exit()
-
-  removeHintMarkers: ->
-    DomUtils.removeElement @hintMarkerContainingDiv if @hintMarkerContainingDiv
-    @hintMarkerContainingDiv = null
-
-# Use characters for hints, and do not filter links by their text.
-class AlphabetHints
-  constructor: ->
-    @linkHintCharacters = Settings.get "linkHintCharacters"
-    # We use the keyChar from keydown if the link-hint characters are all "a-z0-9".  This is the default
-    # settings value, and preserves the legacy behavior (which always used keydown) for users which are
-    # familiar with that behavior.  Otherwise, we use keyChar from keypress, which admits non-Latin
-    # characters. See #1722.
-    @useKeydown = /^[a-z0-9]*$/.test @linkHintCharacters
-    @hintKeystrokeQueue = []
-
-  fillInMarkers: (hintMarkers) ->
-    hintStrings = @hintStrings(hintMarkers.length)
-    for marker, idx in hintMarkers
-      marker.hintString = hintStrings[idx]
-      marker.innerHTML = spanWrap(marker.hintString.toUpperCase())
-
-    hintMarkers
-
-  #
-  # Returns a list of hint strings which will uniquely identify the given number of links. The hint strings
-  # may be of different lengths.
-  #
-  hintStrings: (linkCount) ->
-    hints = [""]
-    offset = 0
-    while hints.length - offset < linkCount or hints.length == 1
-      hint = hints[offset++]
-      hints.push ch + hint for ch in @linkHintCharacters
-    hints = hints[offset...offset+linkCount]
-
-    # Shuffle the hints so that they're scattered; hints starting with the same character and short hints are
-    # spread evenly throughout the array.
-    return hints.sort().map (str) -> str.reverse()
-
-  getMatchingHints: (hintMarkers) ->
-    matchString = @hintKeystrokeQueue.join ""
-    linksMatched: hintMarkers.filter (linkMarker) -> linkMarker.hintString.startsWith matchString
-
-  pushKeyChar: (keyChar, keydownKeyChar) ->
-    @hintKeystrokeQueue.push (if @useKeydown then keydownKeyChar else keyChar)
-  popKeyChar: -> @hintKeystrokeQueue.pop()
-
-# Use numbers (usually) for hints, and also filter links by their text.
-class FilterHints
-  constructor: ->
-    @linkHintNumbers = Settings.get "linkHintNumbers"
-    @hintKeystrokeQueue = []
-    @linkTextKeystrokeQueue = []
-    @activeHintMarker = null
-    # The regexp for splitting typed text and link texts.  We split on sequences of non-word characters and
-    # link-hint numbers.
-    @splitRegexp = new RegExp "[\\W#{Utils.escapeRegexSpecialCharacters @linkHintNumbers}]+"
-
-  generateHintString: (linkHintNumber) ->
-    base = @linkHintNumbers.length
-    hint = []
-    while 0 < linkHintNumber
-      hint.push @linkHintNumbers[Math.floor linkHintNumber % base]
-      linkHintNumber = Math.floor linkHintNumber / base
-    hint.reverse().join ""
-
-  renderMarker: (marker) ->
-    marker.innerHTML = spanWrap(marker.hintString +
-        (if marker.showLinkText then ": " + marker.linkText else ""))
-
-  fillInMarkers: (hintMarkers) ->
-    @renderMarker marker for marker in hintMarkers
-
-    # We use @filterLinkHints() here (although we know that all of the hints will match) to fill in the hint
-    # strings.  This ensures that we always get hint strings in the same order.
-    @filterLinkHints hintMarkers
-
-  getMatchingHints: (hintMarkers, tabCount = 0) ->
-    # At this point, linkTextKeystrokeQueue and hintKeystrokeQueue have been updated to reflect the latest
-    # input. use them to filter the link hints accordingly.
-    matchString = @hintKeystrokeQueue.join ""
-    linksMatched = @filterLinkHints hintMarkers
-    linksMatched = linksMatched.filter (linkMarker) -> linkMarker.hintString.startsWith matchString
-
-    # Visually highlight of the active hint (that is, the one that will be activated if the user
-    # types <Enter>).
-    tabCount = ((linksMatched.length * Math.abs tabCount) + tabCount) % linksMatched.length
-    @activeHintMarker?.classList.remove "vimiumActiveHintMarker"
-    @activeHintMarker = linksMatched[tabCount]
-    @activeHintMarker?.classList.add "vimiumActiveHintMarker"
-
-    linksMatched: linksMatched
-    userMightOverType: @hintKeystrokeQueue.length == 0 and 0 < @linkTextKeystrokeQueue.length
-
-  pushKeyChar: (keyChar, keydownKeyChar) ->
-    # For filtered hints, we *always* use the keyChar value from keypress, because there is no obvious and
-    # easy-to-understand meaning for choosing one of keyChar or keydownKeyChar (as there is for alphabet
-    # hints).
-    if 0 <= @linkHintNumbers.indexOf keyChar
-      @hintKeystrokeQueue.push keyChar
-    else
-      # Since we might renumber the hints, we should reset the current hintKeyStrokeQueue.
-      @hintKeystrokeQueue = []
-      @linkTextKeystrokeQueue.push keyChar
-
-  popKeyChar: ->
-    @hintKeystrokeQueue.pop() or @linkTextKeystrokeQueue.pop()
-
-  # Filter link hints by search string, renumbering the hints as necessary.
-  filterLinkHints: (hintMarkers) ->
-    linkSearchString = @linkTextKeystrokeQueue.join("").trim().toLowerCase()
-    do (scoreFunction = @scoreLinkHint linkSearchString) ->
-      linkMarker.score = scoreFunction linkMarker for linkMarker in hintMarkers
-    hintMarkers = hintMarkers[..].sort (a,b) ->
-      if b.score == a.score then b.stableSortCount - a.stableSortCount else b.score - a.score
-
-    linkHintNumber = 1
-    for linkMarker in hintMarkers
-      continue unless 0 < linkMarker.score
-      linkMarker.hintString = @generateHintString linkHintNumber++
-      @renderMarker linkMarker
-      linkMarker
-
-  # Assign a score to a filter match (higher is better).  We assign a higher score for matches at the start of
-  # a word, and a considerably higher score still for matches which are whole words.
-  scoreLinkHint: (linkSearchString) ->
-    searchWords = linkSearchString.trim().split @splitRegexp
-    (linkMarker) =>
-      text = linkMarker.linkText.trim()
-      linkWords = linkMarker.linkWords ?= text.toLowerCase().split @splitRegexp
-
-      searchWordScores =
-        for searchWord in searchWords
-          linkWordScores =
-            for linkWord, idx in linkWords
-              if linkWord == searchWord
-                if idx == 0 then 8 else 6
-              else if linkWord.startsWith searchWord
-                if idx == 0 then 4 else 2
-              else if 0 <= linkWord.indexOf searchWord
-                1
-              else
-                0
-          Math.max linkWordScores...
-
-      if 0 in searchWordScores
-        0
-      else
-        addFunc = (a,b) -> a + b
-        score = searchWordScores.reduce addFunc, 0
-        # Prefer matches in shorter texts.  To keep things balanced for links without any text, we just weight
-        # them as if their length was 50.
-        score / Math.log 1 + (text.length || 50)
-
-#
-# Make each hint character a span, so that we can highlight the typed characters as you type them.
-#
-spanWrap = (hintString) ->
-  innerHTML = []
-  for char in hintString
-    innerHTML.push("" + char + "")
-  innerHTML.join("")
-
 # Suppress all keyboard events until the user stops typing for sufficiently long.
 class TypingProtector extends Mode
   constructor: (delay, callback) ->


i18n for jade templates

i18n for jade templates

--- src/core/server/index.iced
+++ src/core/server/index.iced
@@ -34,6 +34,7 @@
 configure = ->
   app.locals.name = oConfig.app.name
   app.locals.VERSION = version
+  app.locals.t = i18n.t

   app.set 'x-powered-by', oConfig.xPoweredBy or yes
   app.set 'view engine', 'jade'


Documentation on statics

Documentation on statics

--- src/core/view-model.coffee
+++ src/core/view-model.coffee
@@ -69,7 +69,9 @@
   # @option options [Array|String] internals an array of atttributes that should be scoped with an underscore, eg. name -> _name
   # @option options [Array|String] requires an array of atttributes that will have kb.Observables created even if they do not exist on the Model. Useful for binding Views that require specific observables to exist
   # @option options [Array|String] keys restricts the keys used on a model. Useful for reducing the number of kb.Observables created from a limited set of Model attributes
-  # @option options [Object|Array|String] if an array is supplied, excludes keys to exclude on the view model; for example, if you want to provide a custom implementation. If an Object, it provides options to the kb.Observable constructor.
+  # @option options [Object|Array|String] excludes if an array is supplied, excludes keys to exclude on the view model; for example, if you want to provide a custom implementation. If an Object, it provides options to the kb.Observable constructor.
+  # @option options [Array] statics creates non-observable properties on your view model for Model attributes that do not need to be observed for changes.
+  # @option options [Object] static_defaults provides default values for statics.
   # @option options [String] path the path to the value (used to create related observables from the factory).
   # @option options [kb.Store] store a store used to cache and share view models.
   # @option options [Object] factories a map of dot-deliminated paths; for example `{'models.name': kb.ViewModel}` to either constructors or create functions. Signature: `{'some.path': function(object, options)}`


Provide empty `Controller#beforeAction`, makes it safe to do `super`.

--- src/chaplin/controllers/controller.coffee
+++ src/chaplin/controllers/controller.coffee
@@ -22,6 +22,9 @@
     @initialize arguments...

   initialize: ->
+    # Empty per default.
+
+  beforeAction: ->
     # Empty per default.

   # Change document title.


refactor(macTime): Removed calling digest cycle when highlighting text

--- src/directives/time.coffee
+++ src/directives/time.coffee
@@ -37,7 +37,7 @@

         inputDOM        = $("input", element)[0]
         timeRegex       = /(\d+):(\d+) ([AP]M)/
-        highlighActions =
+        highlightActions =
           hours:   -> inputDOM.setSelectionRange 0, 2
           minutes: -> inputDOM.setSelectionRange 3, 5
           markers: -> inputDOM.setSelectionRange 6, 8
@@ -101,8 +101,8 @@

           # Highlight hour on activating the time input
           $timeout ->
-            inputSelectAction start, end, highlighActions
-          , 0
+            inputSelectAction start, end, highlightActions
+          , 0, false

         #
         # @name $scope.updateScopeTime


Fix youtrack

Fix youtrack

--- plugins/youtrack-issues/index.coffee
+++ plugins/youtrack-issues/index.coffee
@@ -31,6 +31,7 @@
         callback err
       .end (res) ->
         return callback res.error if res.error
+        return callback "401: Unable to login - please check your credentials" unless res.headers['set-cookie']
         callback null, handleCookies res

   @createTicket = (config, event, token, callback) ->


moved 'section-title' className in refbook nav

--- src/components/reference-book/navbar.cjsx
+++ src/components/reference-book/navbar.cjsx
@@ -23,9 +23,11 @@
     {section, ecosystemId} = @props
     title = ReferenceBookStore.getPageTitle({section, ecosystemId})

-    <BS.Nav navbar className="section-title">
-      <ChapterSection section={section} />
-      {title}
+    <BS.Nav navbar>
+      <div className="section-title">
+        <ChapterSection section={section} />
+        {title}
+      </div>
     </BS.Nav>

   render: ->


Null check for failedData and only iterate arrays

--- src/concept-coach/error-notification.cjsx
+++ src/concept-coach/error-notification.cjsx
@@ -16,9 +16,9 @@
     api.channel.off 'error', @onError

   onError: ({response, failedData}) ->
-    return if failedData.stopErrorDisplay # someone else is handling displaying the error
+    return if failedData?.stopErrorDisplay # someone else is handling displaying the error
     errors = ["#{response.status}: #{response.statusText}"]
-    if failedData.data?.errors # we have something from server to display
+    if _.isArray(failedData.data?.errors) # we have something from server to display
       errors = errors.concat(
         _.flatten _.map failedData.data.errors, (error) ->
           # All 422 errors from BE *should* have a "code" property.  If not, show whatever it is


assign to _local directly istead of calling loaded

Reverts change in commit 8b151dadd1b28ea7c77c9af083da5bd65ef91182

For some unknown reason that causes the task steps to repeat the first step
when the task is completed

--- tutor/src/flux/task.coffee
+++ tutor/src/flux/task.coffee
@@ -109,7 +109,8 @@

   stepCompleted: (obj, taskStepId) ->
     TaskStepActions.completed(obj, taskStepId)
-    this.loaded(obj, obj.id)
+    this._loaded(obj, obj.id)
+    Object.assign(@_local[obj.id], obj)
     @emit('step.completed', taskStepId, obj.id)

   exports:


Update client.coffee

Update client.coffee

--- src/examples/client.coffee
+++ src/examples/client.coffee
@@ -2,7 +2,6 @@

 builder = Client.Builder()

-c = null
 builder.setClientId("8a1143fe5910ed00ec1489ce")
   .setClientSecret("c12516026334fb8dd0890090bfe2d25392e2ce2f7a8f177f")
   .setApiUri("http://localhost:8080/")


don't require restart by listening to changes

--- lib/connection.coffee
+++ lib/connection.coffee
@@ -9,11 +9,12 @@
     @client.boot = => @boot()
     @process.activate()

-    @client.handle 'error', (options) =>
-      if atom.config.get("julia-client.errorsToConsole")
-        @process.emitter.emit 'stderr', options.msg + '\n' + options.detail
-      else
-        atom.notifications.addError options.msg, options
+    atom.config.observe 'julia-client.errorsToConsole', (val) =>
+      @client.handle 'error', (options) =>
+        if val
+          @process.emitter.emit 'stderr', options.msg + '\n' + options.detail
+        else
+          atom.notifications.addError options.msg, options

     if atom.config.get("julia-client.launchOnStartup")
       atom.commands.dispatch atom.views.getView(atom.workspace),


doh, remove debugging

doh, remove debugging

--- lib/controller.coffee
+++ lib/controller.coffee
@@ -192,7 +192,6 @@
         .then(onSuccess, onError)

     if @activeRepl
-      @activeRepl.warnIsNotCompiled()
       # if stuck in compile error
       # then post warning and return
       unless @activeRepl.isCompiled()


Added ngInject

Added ngInject

--- app/assets/javascripts/jquest_pg/application.js.coffee
+++ app/assets/javascripts/jquest_pg/application.js.coffee
@@ -1,6 +1,7 @@
 #= require_tree .
 angular.module 'jquest'
   .run (Menu)->
+      'ngInject'
       # Menus items
       Menu.addItem name: "Watch intro again", state: 'main.pg.intro', category: 'Tutorials'
       Menu.addItem name: "How-to", state: 'main.pg.tutorials', category: 'Tutorials'
@@ -9,9 +10,11 @@
       Menu.addItem name: "Collected data", state: 'main.pg.data', category: 'Your mission'
       Menu.addItem name: "About", state: 'main.pg.about'
   .run ($rootScope, $state)->
+    'ngInject'
     $rootScope.$on '$stateChangeSuccess', (ev, state)->
       $state.go 'main.pg' if state.name is 'main'
   .config ($stateProvider)->
+    'ngInject'
     $stateProvider
       .state 'main.pg',
         template: '
ok
'


fixes #1550

fixes #1550

--- app/assets/javascripts/modules/blaze/filters.js.coffee
+++ app/assets/javascripts/modules/blaze/filters.js.coffee
@@ -46,4 +46,8 @@

 kor.filter 'human_date', ["korTranslate", (kt) ->
   (input) -> kt.localize input
+]
+
+kor.filter 'trust_as_url', ["$sce", (sce) ->
+  (input) -> if input then sce.trustAsUrl(input) else ""
 ]


OX-448 new garbage collection

OX-448 new garbage collection

--- app/assets/javascripts/oxalis/model/binary/cube.coffee
+++ app/assets/javascripts/oxalis/model/binary/cube.coffee
@@ -186,7 +186,6 @@
         bucket.accessed = false
         @bucketIterator = ++@bucketIterator % MAXIMUM_BUCKET_COUNT

-      console.log "Adding:", address, "Removing:", bucket, @bucketIterator
       @collectBucket(bucket)
       @bucketCount--


remove console.log in theme_actions.coffee

--- app/assets/javascripts/para/admin/theme_actions.coffee
+++ app/assets/javascripts/para/admin/theme_actions.coffee
@@ -9,7 +9,6 @@
     scrollToComponentSection($activeItem)

   $('.component-item').on 'click', 'a', (e) ->
-    console.log 'scroll to ', $(e.currentTarget).closest('.component-item')
     scrollToComponentSection($(e.currentTarget), 150)

 $(document).on 'page:change', ->


Allow series ordering via @options.series

--- app/assets/javascripts/pgdatagraph/datagraph.js.coffee
+++ app/assets/javascripts/pgdatagraph/datagraph.js.coffee
@@ -1,5 +1,4 @@
 # Graph looks weird with series that have extreme drops to x: 0
-# Series label get messed up when dragging brush

 class PG.DataGraph

@@ -13,9 +12,7 @@
       { label: "last 24 hours", duration: "-1d" }
     ]
     dateFormat: "M d, yy"
-    series: {
-      renderer: "line"
-    }
+    renderer: "line"
     detailSmoothing: 10
     overviewSmoothing: 10
     yAxisTickFormat: (y) -> y
@@ -79,8 +76,19 @@
   getSeries: (data) ->
     @seriesDataNames = {}
     series = []
-    _(data).each (seriesData, name) =>
-      seriesData = _.map seriesData, (s) -> { x: s[0], y: s[1] }
+
+    initialOrder = _.keys(data)
+
+    if @options.series
+      seriesOrder = _.keys(@options.series)
+      for name in seriesOrder
+        initialOrder = _.without initialOrder, name
+      initialOrder = seriesOrder.concat(initialOrder)
+
+    @seriesOrder = initialOrder unless @seriesOrder?
+
+    for name in @seriesOrder
+      seriesData = _.map data[name], (s) -> { x: s[0], y: s[1] }
       if @seriesColors[name]?
         color = @seriesColors[name]
       else
@@ -93,7 +101,7 @@
         stroke = no
       else
         stroke = color.stroke
-      renderer = @options.series[name]?.renderer or @options.series.renderer
+      renderer = @options.series[name]?.renderer or @options.renderer
       seriesName = @options.series[name]?.name or name
       @seriesDataNames[seriesName] = name
       series.push {


comment review (cont)

comment review (cont)

--- src/arguegraph.coffee
+++ src/arguegraph.coffee
@@ -59,16 +59,12 @@
     result.push el if not (el in A)
   result

-# An ArgumentFramework wraps a map of defeats* that defines the argument network
-# and provides functions for interrogating that network.
-# Errors are thrown for inconsistent/malformed networks and queries
-#
-# * The word "defeats" is used widely in the literature, but it may
-# not be the best term to use, as it suggests a resolved struggle whereas in
-# this formalisation it is possible for two arguments to simultaneously "defeat"
-# one another. It is the job of the algorithms to establish which arguments can
-# be said to defend themselves.  Alternative terms that could be used here
-# include "attack" and "conflict with".
+# An ArgumentFramework wraps a map of defeats* (called the defeatermap) that
+# defines the argument network and provides functions for interrogating that
+# network. Errors are thrown for inconsistent/malformed networks and queries.
+# * The term "defeats" is widely used in the supporting literature, but it may
+# not be the best word to use, as it suggests a resolved struggle.  Alternative
+# terms that could be used here include "attack" and "conflict with".
 class ArgumentFramework
   # defeatermap: object whose keys are arguments and whose values are arrays of defeating arguments
   constructor: (@defeatermap={}) ->


2014-03-12

--- tasks/sass-format.coffee
+++ tasks/sass-format.coffee
@@ -45,28 +45,89 @@
             tmp = grunt.file.read(f)
             fs = require('fs')
             text = []
-            line = 0
+            line = 1
             indent = 0
+            msg = ''
+
             fs.readFileSync(f).toString().split('\n').forEach(
               (txt) ->
-                if /^([ \t]+)?.*{([ \t]+)?$/.test(txt)
-                  grunt.log.warn 'start:'+indent+'===================='
+                text.push(txt)
+            )
+
+            #console.log text
+
+            for i in [0..text.length-1]
+              #セレクタ記述
+              if /^([ \t]+)?.*[{,]([ \t]+)?$/.test(text[i])
+                #grunt.log.warn 'start:'+indent+'===================='
+
+                # セレクタ後スペース有無チェック
+                if /[^ ][ ][{,]$/.test(text[i])
+                  msg += 'スペース有り'
+                if /[^ ][ ]{2,}[{,]$/.test(text[i])
+                  msg += '!!スペース多い!!'
+                if /[^ ][{,]$/.test(text[i])
+                  msg += '!!スペースなし!!'
+
+                # セレクタ並び方チェック
+                if /^[^,]+[{,]$/.test(text[i])
+                  msg += '1行1プロパティ'
+                if /^([^,]+,){1,}[^,{]+{$/.test(text[i])
+                  msg += '!!1行に複数ある!!'
+
+                # セレクタ記述終了
+                if /^([ \t]+)?.*{([ \t]+)?$/.test(text[i])
                   indent++

-                  if /[ ]{/.test(txt)
-                    grunt.log.ok 'スペース有り'
-                  else
-                    grunt.log.warn '!スペースなし!'
+                console.log line+': '+msg+': '+text[i]
+                msg = ''

-                    console.log line+': '+txt
-                #console.log line+': '+txt
-                #text.push(txt)
-                line++
+              #セレクタ記述
+              if /^[^;]+;$/.test(text[i])
+                #インデントチェック
+                if /^[\t]{1,}/.test(text[i])
+                  msg += 'インデントがタブ'
+                if /^[ ]{1,}/.test(text[i])
+                  msg += 'インデントがホワイトスペース'

-                if /^([ \t]+)?.*}([ \t]+)?$/.test(txt)
-                  indent--
-                  grunt.log.ok 'end:'+indent+'===================='
-            )
+                console.log line+': '+msg+': '+text[i]
+                msg = ''
+
+              line++
+
+              if /^([ \t]+)?.*}([ \t]+)?$/.test(text[i])
+                indent--
+                msg = ''
+                #grunt.log.ok 'end:'+indent+'===================='
+            #fs.readFileSync(f).toString().split('\n').forEach(
+            #  (txt) ->
+            #text.forEach(
+            #  (txt) ->
+            #    if /^([ \t]+)?.*[{,]([ \t]+)?$/.test(txt)
+            #      grunt.log.warn 'start:'+indent+'===================='
+
+            #      #
+
+
+            #      if /^([ \t]+)?.*{([ \t]+)?$/.test(txt)
+            #        indent++
+
+            #      if /[ ][{,]$/.test(txt)
+            #        grunt.log.ok 'スペース有り'
+            #      if /[ ]{2,}[{,]$/.test(txt)
+            #        grunt.log.warn '!スペース多い!'
+            #      if /[^ ][{,]$/.test(txt)
+            #        grunt.log.warn '!スペースなし!'
+
+            #      console.log line+': '+txt
+            #    #console.log line+': '+txt
+            #    #text.push(txt)
+            #    line++
+
+            #    if /^([ \t]+)?.*}([ \t]+)?$/.test(txt)
+            #      indent--
+            #      grunt.log.ok 'end:'+indent+'===================='
+            #)

             #console.log text


Add spec for the word offset

Add spec for the word offset

--- spec/autocomplete-manager-integration-spec.coffee
+++ spec/autocomplete-manager-integration-spec.coffee
@@ -383,8 +383,17 @@
           left = editorView.pixelPositionForBufferPosition([0, 2]).left
           expect(overlayElement.style.left).toBe "#{gutterWidth + left}px"

-          atom.commands.dispatch(editorView, 'autocomplete-plus:cancel')
-          expect(editorView.querySelector('.autocomplete-plus')).not.toExist()
+      it "displays the suggestion list with a negative margin to align the prefix with the word-container", ->
+        spyOn(provider, 'getSuggestions').andCallFake (options) ->
+          [{text: 'ab', leftLabel: 'void'}, {text: 'abc', leftLabel: 'void'}]
+
+        editor.insertText('omghey ab')
+        triggerAutocompletion(editor, false, 'c')
+
+        runs ->
+          suggestionList = editorView.querySelector('.autocomplete-plus autocomplete-suggestion-list')
+          wordContainer = editorView.querySelector('.autocomplete-plus autocomplete-suggestion-list .word-container')
+          expect(suggestionList.style['margin-left']).toBe "-#{wordContainer.offsetLeft}px"

       it "keeps the suggestion list planted at the beginning of the prefix when typing", ->
         overlayElement = null


client/socialapicontroller: update createChannelParticipants

--- client/app/lib/socialapicontroller.coffee
+++ client/app/lib/socialapicontroller.coffee
@@ -902,7 +902,7 @@
       { id } = options
       doXhrRequest
         type     : 'POST'
-        endPoint : "/api/social/channel/#{id}/update"
+        endPoint : "/api/social/channel/initwithparticipants"
         data     : options
       , callback


Fixed relative links in posts.

Fixed relative links in posts.

--- client/coffeescript/templates/post.coffee
+++ client/coffeescript/templates/post.coffee
@@ -6,10 +6,10 @@
     return post_content_html
   identifier: -> @_id
   link_href: ->
-    return "#{ @_id }"
+    return "/#{ @_id }"
   parent_href: ->
     if @parent_id?
-      return "#{ @parent_id }"
+      return "/#{ @parent_id }"
     else
       return false


added --no-color to the default arguments for boot

--- lib/proto-repl.coffee
+++ lib/proto-repl.coffee
@@ -40,7 +40,7 @@
     bootArgs:
       description: 'The arguments to be passed to boot. For advanced users only.'
       type: 'string'
-      default: "repl"
+      default: "--no-colors repl"
     showInlineResults:
       description: "Shows inline results of code execution. Install Atom Ink package to use this."
       type: 'boolean'


Obtain editor from atom.workspace

Obtain editor from atom.workspace

Closes #12

--- lib/sort-lines.coffee
+++ lib/sort-lines.coffee
@@ -15,7 +15,7 @@
       uniqueLines(editor)

     atom.workspaceView.command 'sort-lines:case-insensitive-sort', '.editor', ->
-      editor = atom.workspaceView.getActivePaneItem()
+      editor = atom.workspace.getActiveEditor()
       sortLinesInsensitive(editor)

 sortLines = (editor) ->


:lipstick:

:lipstick:

--- lib/tag-finder.coffee
+++ lib/tag-finder.coffee
@@ -13,7 +13,7 @@
     @tagSelector = new ScopeSelector('meta.tag | punctuation.definition.tag')
     @commentSelector = new ScopeSelector('comment.*')

-  getTagPattern: (tagName) ->
+  patternForTagName: (tagName) ->
     new RegExp("(<#{tagName}([\\s>]|$))|(</#{tagName}>)", 'gi')

   isRangeCommented: (range) ->
@@ -25,9 +25,10 @@

   findStartTag: (tagName, endPosition) ->
     scanRange = new Range([0, 0], endPosition)
+    pattern = @patternForTagName(tagName)
     startRange = null
     unpairedCount = 0
-    @editor.backwardsScanInBufferRange @getTagPattern(tagName), scanRange, ({match, range, stop}) =>
+    @editor.backwardsScanInBufferRange pattern, scanRange, ({match, range, stop}) =>
       return if @isRangeCommented(range)

       if match[1]
@@ -42,9 +43,10 @@

   findEndTag: (tagName, startPosition) ->
     scanRange = new Range(startPosition, @editor.buffer.getEndPosition())
+    pattern = @patternForTagName(tagName)
     endRange = null
     unpairedCount = 0
-    @editor.scanInBufferRange @getTagPattern(tagName), scanRange, ({match, range, stop}) =>
+    @editor.scanInBufferRange pattern, scanRange, ({match, range, stop}) =>
       return if @isRangeCommented(range)

       if match[1]


clean up a bit

clean up a bit

--- lib/xml-parser.coffee
+++ lib/xml-parser.coffee
@@ -1,7 +1,5 @@
 {EventEmitter} = require 'events'
-sax = require('sax')
-
-util = require 'util'
+sax = require 'sax'

 module.exports =
 class Parser
@@ -13,8 +11,6 @@
     stack = []
     @parser = sax.parser(strict, options)

-    # opencdata, cdata, closecdata
-
     @parser.onerror = (e) ->
       # an error happened.
       return
@@ -28,11 +24,12 @@
       return

     @parser.onopentag = (node) ->
+      # opened a tag.  node has "name" and "attributes"
       stack.push(node)
-      # opened a tag.  node has "name" and "attributes"
       return

     @parser.onclosetag = (name) =>
+      # closed a tag. "name" is tag name
       node = stack.pop()
       result = {}
       res2 = {}
@@ -45,14 +42,15 @@
         parent = stack[stack.length - 1]
         parent.children ?= []
         parent.children.push(result)
-      # closed a tag. "name" is tag name
       return
-
-

   write: (text) ->
     @parser.write(text)
-
+
+  # Supported events:
+  #
+  # * "command"
+  #
   on: (event, cb) ->
     @events.on event, cb


Use local functions instead of methods in y-richtext

--- lib/y-richtext.coffee
+++ lib/y-richtext.coffee
@@ -87,7 +87,7 @@
   passDeltas : (deltas) => @locker.try deltas, (deltas) =>
     position = 0
     for delta in deltas.ops
-      position = @deltaHelper delta, position
+      position = deltaHelper @, delta, position

   # @override updateCursorPosition(index)
   #   update the position of our cursor to the new one using an index
@@ -154,9 +154,10 @@
   # @param position [Integer] start position for the delta, default: 0
   #
   # @return [Integer] the position of the cursor after parsing the delta
-  deltaHelper= (delta, position = 0) =>
+  deltaHelper = (thisObj, delta, position = 0) ->
     if delta?
-      selections = (@_get "selections")
+      console.log thisObj._get
+      selections = (thisObj._get "selections")
       delta_unselections = []
       delta_selections = {}
       for n,v of delta.attributes
@@ -166,41 +167,41 @@
           delta_unselections.push n

       if delta.insert?
-        @insertHelper position, delta.insert
-        from = @_get("characters").ref(position)
-        to = @_get("characters").ref(position+delta.insert.length)
-        @_get("selections").select from, to, delta_selections
-        @_get("selections").unselect from, to, delta_unselections
+        insertHelper thisObj, position, delta.insert
+        from = thisObj._get("characters").ref(position)
+        to = thisObj._get("characters").ref(position+delta.insert.length)
+        thisObj._get("selections").select from, to, delta_selections
+        thisObj._get("selections").unselect from, to, delta_unselections

         return position + delta.insert.length

       else if delta.delete?
-        @deleteHelper position, delta.delete
+        deleteHelper thisObj, position, delta.delete
         return position

       else if delta.retain?
         retain = parseInt delta.retain
-        from = @_get("characters").ref(position)
-        to = @_get("characters").ref(position + retain)
-
-        @_get("selections").select from, to, delta_selections
-        @_get("selections").unselect from, to, delta_unselections
+        from = thisObj._get("characters").ref(position)
+        to = thisObj._get("characters").ref(position + retain)
+
+        thisObj._get("selections").select from, to, delta_selections
+        thisObj._get("selections").unselect from, to, delta_unselections

         return position + retain
       throw new Error "This part of code must not be reached!"

-  insertHelper= (position, content) =>
+  insertHelper = (thisObj, position, content) ->
     as_array =
       if typeof content == "string"
         content.split("")
       else if typeof content == "number"
         [content]
     if as_array?
-      @_get("characters").insertContents position, as_array
-
-  deleteHelper= (position, length = 1) =>
+      thisObj._get("characters").insertContents position, as_array
+
+  deleteHelper = (thisObj, position, length = 1) ->
     console.log "deleteHelper"
-    (@_get "characters").delete position, length
+    (thisObj._get "characters").delete position, length

 if window?
   if window.Y?


Clean code

Clean code

--- lib/zen.config.coffee
+++ lib/zen.config.coffee
@@ -19,8 +19,7 @@
     ZEN[attribute] = value for attribute, value of environment

   # -- ZEN port ----------------------------------------------------------------
-  ZEN.port = process.argv[4] if process.argv[4]?
-  ZEN.port = process.env.PORT if process.env.PORT?
+  ZEN.port = process.argv[4] or process.env.PORT or 1337

   # -- ZEN timezone ------------------------------------------------------------
   process.env.TZ = ZEN.timezone if ZEN.timezone


_fetchFromRegistry: Use _fetchFromHTTP if the pkg info doesn't contain a shasum

This has the added benefit of using the package.json that will actually be used by npm (i.e. the one in the tarballl) to determine dependencies and package information, if we have to fetch the tarball anyway

--- src/package-fetcher.coffee
+++ src/package-fetcher.coffee
@@ -59,19 +59,14 @@

 PackageFetcher.prototype._fetchFromRegistry = (name, spec, registry) ->
   handlePackage = (pkg) =>
-    @_handleDeps pkg, registry
     unless pkg.dist.tarball?
       @emit 'error', "Could not find supported dist type for #{pkg.name}@#{pkg.version} in #{util.inspect pkg.dist}", name, spec
     else
       if pkg.dist.shasum? # Sometimes npm gives us shasums, how nice
+        @_handleDeps pkg, registry
         @emit 'fetched', name, spec, pkg
       else
-        fetchUrl pkg.dist.tarball, (err, sha256sum) =>
-          if err?
-            @emit 'error', "Error getting hash of #{pkg.dist.tarball} for #{pkg.name}@#{pkg.version}: #{err}", name, spec
-          else
-            pkg.dist.sha256sum = sha256sum
-            @emit 'fetched', name, spec, pkg
+        @_fetchFromHTTP name, spec, registry, url.parse dist.tarball

   registry.get "/#{encodeURIComponent name}", (err, info) =>
     if err?
@@ -202,37 +197,4 @@
     else
       @fetch nm, dep, registry

-fetchUrl = do ->
-  cache = {}
-  (u, callback) ->
-    if cache[u]?
-      callback undefined, cache[u]
-    else
-      parsed = url.parse u
-      client = switch parsed.protocol
-        when 'http:'
-          http
-        when 'https:'
-          https
-        else
-          undefined
-      unless client?
-        callback "Unsupported protocol #{parsed.protocol}"
-      else
-        client.get u, (res) ->
-          unless res.statusCode is 200
-            callback "Unsuccessful status code while GETting #{u}: #{http.STATUS_CODES[res.statusCode]}"
-          else
-            res.on 'error', (err) -> callback "Error while GETting #{u}: #{err}"
-            hash = crypto.createHash 'sha256'
-            hash.on 'error', (err) ->
-              callback "Error calculating hash for #{u}: #{err}"
-            res.pipe hash
-            hash.on 'readable', ->
-              hashBuffer = hash.read 32
-              if hashBuffer?
-                hashString = hashBuffer.toString 'hex'
-                cache[u] = hashString
-                callback hashString
-
 module.exports = PackageFetcher


Add PackageManager::isBundledPackage

Add PackageManager::isBundledPackage

--- src/package-manager.coffee
+++ src/package-manager.coffee
@@ -194,6 +194,18 @@
     {engines} = Package.loadMetadata(packagePath, true)
     engines?.atom?

+  isBundledPackage: (packageName) ->
+    @getPackageDependencies().hasOwnProperty(packageName)
+
+  getPackageDependencies: ->
+    unless @packageDependencies?
+      try
+        metadataPath = path.join(@resourcePath, 'package.json')
+        {@packageDependencies} = JSON.parse(fs.readFileSync(metadataPath)) ? {}
+        @packageDependencies ?= {}
+
+    @packageDependencies
+
   getAvailablePackagePaths: ->
     packagePaths = []

@@ -201,9 +213,6 @@
       for packagePath in fs.listSync(packageDirPath)
         packagePaths.push(packagePath) if fs.isDirectorySync(packagePath)

-    try
-      metadataPath = path.join(@resourcePath, 'package.json')
-      {packageDependencies} = JSON.parse(fs.readFileSync(metadataPath)) ? {}
     packagesPath = path.join(@resourcePath, 'node_modules')
     for packageName, packageVersion of packageDependencies ? {}
       packagePath = path.join(packagesPath, packageName)


[LFTUI-90] fetching the new stream permissions for a stream after the user creates it

--- src/coffee/directives/user_streams.coffee
+++ src/coffee/directives/user_streams.coffee
@@ -16,11 +16,22 @@
       found

     makeStream = (stream) ->
-      success = (created_stream) ->
+      created_stream = null
+
+      loadedPermissions = (permissions) ->
+        for p in permissions
+          $scope.permissions.push p
+
         $scope.streams.push created_stream
+
         $scope.new_stream =
           title: ''
           description: ''
+
+      success = (s) ->
+        created_stream = s
+        (Api.StreamPermission.query
+          stream: created_stream.id).$promise.then loadedPermissions, fail

       fail = () ->
         failed_lang = Lang 'streams.errors.create'


jupyter: disable interrupt button if not running

--- src/smc-webapp/jupyter/top-buttonbar.cjsx
+++ src/smc-webapp/jupyter/top-buttonbar.cjsx
@@ -26,11 +26,13 @@
             cur_id              : rtypes.string          # id of currently selected cell
             sel_ids             : rtypes.immutable.Set   # set of selected cells
             has_unsaved_changes : rtypes.bool
+            kernel_state        : rtypes.string

     shouldComponentUpdate: (next) ->
         return next.cur_id != @props.cur_id or \
             next.cells?.getIn([@props.cur_id, 'cell_type']) != @props.cells?.getIn([@props.cur_id, 'cell_type']) or \
-            next.has_unsaved_changes != @props.has_unsaved_changes
+            next.has_unsaved_changes != @props.has_unsaved_changes or \
+            next.kernel_state != @props.kernel_state

     command: (name, focus) ->
         return =>
@@ -42,11 +44,13 @@
                 @props.actions.blur()

     render_button: (key, name) ->
+        if typeof(name) == 'object'
+            {name, disabled} = name
         obj = @props.actions._commands?[name]
         if not obj?
             return
         focus = not misc.endswith(obj.m, '...')
-        <Button key={key} onClick={@command(name, focus)} title={obj.m} >
+        <Button key={key} onClick={@command(name, focus)} title={obj.m} disabled={disabled} >
             <Icon name={obj.i}/>
         </Button>

@@ -69,7 +73,7 @@
         @render_button_group(['move cell up', 'move cell down'])

     render_group_run: ->
-        @render_button_group(['run cell and select next', 'interrupt kernel'])
+        @render_button_group(['run cell and select next', {name:'interrupt kernel', disabled:@props.kernel_state != 'busy'}])

     cell_select_type: (event) ->
         @props.actions.set_selected_cell_type(event.target.value)


Boyscouting FactsView

Boyscouting FactsView

--- core/app/backbone/views/facts_view.coffee
+++ core/app/backbone/views/facts_view.coffee
@@ -1,8 +1,8 @@
 class window.FactsView extends AutoloadingCompositeView
-  tagName: "div"
   className: "facts-view"
   itemViewContainer: ".facts"
   itemView: FactView
+
   events:
     "submit #create_fact_for_channel": "createFact"
     "focus #create_fact_for_channel textarea": "openCreateFactForm"
@@ -10,7 +10,7 @@
     "click #create_fact_for_channel": "focusCreateFactlink"
     "click #create_fact_for_channel .input-box": "focusField"

-  template: "channels/_facts"
+  template: "channels/facts"
   initialize: (options) ->
     @addShowHideToggle "loadingIndicator", "div.loading"
     @collection.on "startLoading", @loadingIndicatorOn, this


Add first (passing) test for sets and setHash with empty values.

--- spec/hashparamsSpec.coffee
+++ spec/hashparamsSpec.coffee
@@ -74,21 +74,20 @@
             it 'invalid type', -> expect(-> new HashParams('value:dszquphsbnt')).toThrowError /Invalid parameter type/
     describe '.setHash()', ->
         params = null
+        hashYieldsValues = (hash, expected) ->
+            params.setHash hash
+            expect(params.values).toEqual expected
+        emptyCases = {
+            'just a hash character': '#',
+            'an empty string': '',
+            'undefined': undefined,
+            'null': null
+        }
         describe 'with scalars "foreground" and "background"', ->
-            beforeEach ->
-                params = new HashParams('foreground', 'background')
-            hashYieldsValues = (hash, expected) ->
-                params.setHash hash
-                expect(params.values).toEqual expected
+            beforeEach -> params = new HashParams('foreground', 'background')
             describe 'resets to empty when passed', ->
                 beforeEach -> params.values = {foreground: 'blue', background: 'green'}
-                cases = {
-                    'just a hash character': '#',
-                    'an empty string': '',
-                    'undefined': undefined,
-                    'null': null
-                }
-                for name, hash of cases
+                for name, hash of emptyCases
                     do (name, hash) ->
                         it name, -> hashYieldsValues hash, {foreground: '', background: ''}
             it 'can set values.foreground', ->
@@ -102,6 +101,13 @@
                 hashYieldsValues '#foreground=blue;background=green', {foreground: 'blue', background: 'green'}
             it 'will not set values.foo', ->
                 hashYieldsValues '#foo=bar', {foreground: '', background: ''}
+        describe 'with sets "tags" and "authors"', ->
+            beforeEach -> params = new HashParams('tags:set', 'authors:set')
+            describe 'resets to empty when passed', ->
+                beforeEach -> params.values = {tags: setOf('A'), authors: setOf('Bob')}
+                for name, hash of emptyCases
+                    do (name, hash) ->
+                        it name, -> hashYieldsValues hash, {tags: setOf(), authors: setOf()}
         describe 'unencoding', ->
             decodes = (encoded, decoded) ->
                 params = new HashParams('name' + decoded)


Fix 64-spec

Fix 64-spec

--- spec/issues/64-spec.coffee
+++ spec/issues/64-spec.coffee
@@ -8,31 +8,29 @@

   describe "Issue 64", ->
     beforeEach ->
-      # Create a fake workspace and open a sample file
-      atom.workspaceView = new WorkspaceView
-      atom.workspaceView.openSync "issues/64.css"
-      atom.workspaceView.simulateDomAttachment()
+      runs ->
+        # Set to live completion
+        atom.config.set "autocomplete-plus.enableAutoActivation", true

-      # Set to live completion
-      atom.config.set "autocomplete-plus.enableAutoActivation", true
+        # Set the completion delay
+        completionDelay = 100
+        atom.config.set "autocomplete-plus.autoActivationDelay", completionDelay
+        completionDelay += 100 # Rendering delay
+        atom.workspaceView = new WorkspaceView()
+        atom.workspace = atom.workspaceView.model

-      # Set the completion delay
-      completionDelay = 100
-      atom.config.set "autocomplete-plus.autoActivationDelay", completionDelay
-      completionDelay += 100 # Rendering delay
+      waitsForPromise -> atom.workspace.open("issues/64.css").then (e) ->
+        editor = e
+        atom.workspaceView.simulateDomAttachment()

       # Activate the package
-      activationPromise = atom.packages.activatePackage "autocomplete-plus"
+      waitsForPromise -> atom.packages.activatePackage("autocomplete-plus").then (a) -> autocomplete = a

-      editorView = atom.workspaceView.getActiveView()
-      {editor} = editorView
-      autocomplete = new AutocompleteView editorView
+      runs ->
+        editorView = atom.workspaceView.getActiveView()
+        autocomplete = new AutocompleteView editorView

     it "it adds words hyphens to the wordlist", ->
-
-      waitsForPromise ->
-        activationPromise
-
       runs ->
         editorView.attachToDom()
         editor.insertText c for c in "bla"


Adding timing sections places

Adding timing sections places

--- src/ThreeBSP.coffee
+++ src/ThreeBSP.coffee
@@ -13,7 +13,7 @@
   value

 class Timelimit
-  constructor: (@timeout=1000) -> "NOTHING"
+  constructor: (@timeout) -> "NOTHING"

   check: =>
     return unless @started?
@@ -26,16 +26,13 @@
     @started ?= Date.now()
     @tasks ?= 0
     @tasks += 1
-    console.log "Starting task, now #{@tasks}"
     do @check

   finish: =>
     throw new Error("Finished more tasks than started") if @tasks? and @tasks < 1
     @tasks -= 1
     elapsed = @check()
-    console.log "Finished task, now: #{@tasks} [#{elapsed}/#{@timeout}]"
     if @tasks == 0
-      console.log "All tasks finished: #{elapsed}/#{@timeout}ms"
       @started = undefined

   doTask: (block) =>
@@ -59,12 +56,8 @@
     @options ?= {}
     @matrix ?= new THREE.Matrix4()

-    console.log "Options:", @options
-    console.log "Matrix:", @matrix
-
-    # Start a timer if we have a timeout
-    if @options.timeout?
-      @options.timer = new Timelimit(@options.timeout)
+    # Start a timer if one wasn't passed
+    @options.timer ?= new Timelimit(@options.timeout)

     @tree   = @toTree treeIsh

@@ -118,7 +111,7 @@
           geometry.faceVertexUvs[0].push vertUvs

   # CSG Operations
-  subtract: (other) =>
+  subtract: (other) => @options.timer.doTask =>
     [us, them] = [@tree.clone(), other.tree.clone()]
     us
       .invert()
@@ -252,12 +245,12 @@
 ##
 ## ThreeBSP.Node
 class ThreeBSP.Node
-  clone: => returning (node = new ThreeBSP.Node()), =>
+  clone: => returning (node = new ThreeBSP.Node(@options)), =>
     node.divider  = @divider?.clone()
-    node.polygons = (p.clone() for p in @polygons)
-    node.front    = @front?.clone()
-    node.back     = @back?.clone()
-    node.options  = @options
+    node.polygons = @options.timer.doTask => (p.clone() for p in @polygons)
+    node.front    = @options.timer.doTask => @front?.clone()
+    node.back     = @options.timer.doTask => @back?.clone()
+

   constructor: (polygons, @options={}) ->
     if polygons? and not (polygons instanceof Array)
@@ -265,15 +258,17 @@
       polygons = undefined

     @polygons = []
-    @options?.timer?.doTask? =>
+    @options.timer.doTask =>
       @build(polygons) if polygons? and polygons.length

   build: (polygons) => returning this, =>
     sides = front: [], back: []
     @divider ?= polygons[0].clone()
-    for poly in polygons
-      @options?.timer?.doTask? =>
-        @divider.subdivide poly, @polygons, @polygons, sides.front, sides.back
+
+    @options.timer.doTask =>
+      for poly in polygons
+        @options.timer.doTask =>
+          @divider.subdivide poly, @polygons, @polygons, sides.front, sides.back

     for own side, polys of sides
       if polys.length


Support to set API and OAuth host.

Support to set API and OAuth host.

--- src/coffee/run.coffee
+++ src/coffee/run.coffee
@@ -75,7 +75,8 @@
       .option '-p, --projectKey <key>', 'your SPHERE.IO project-key'
       .option '-i, --clientId <id>', 'your OAuth client id for the SPHERE.IO API'
       .option '-s, --clientSecret <secret>', 'your OAuth client secret for the SPHERE.IO API'
-      .option '--sphereHost <host>', 'SPHERE.IO API host to connecto to'
+      .option '--sphereHost <host>', 'SPHERE.IO API host to connect to'
+      .option '--sphereAuthHost <host>', 'SPHERE.IO OAuth host to connect to'
       .option '--timeout [millis]', 'Set timeout for requests (default is 300000)', parseInt, 300000
       .option '--verbose', 'give more feedback during action'
       .option '--debug', 'give as many feedback as possible'
@@ -121,6 +122,10 @@
             csvDelimiter: opts.csvDelimiter

           options.host = program.sphereHost if program.sphereHost
+          if program.sphereAuthHost
+            options.oauth_host = program.sphereAuthHost
+            options.rejectUnauthorized = false
+
           options.continueOnProblems = opts.continueOnProblems or false

           # if program.verbose
@@ -187,6 +192,11 @@
             #   ]

           options.host = program.sphereHost if program.sphereHost
+          if program.sphereAuthHost
+            options.oauth_host = program.sphereAuthHost
+            options.rejectUnauthorized = false
+
+          options.host = program.sphereHost if program.sphereHost

           # if program.verbose
           #   options.logConfig.streams = [
@@ -277,6 +287,9 @@
               queryType: opts.queryType
               isQueryEncoded: opts.queryEncoded or false
           options.client.host = program.sphereHost if program.sphereHost
+          if program.sphereAuthHost
+            options.client.oauth_host = program.sphereAuthHost
+            options.client.rejectUnauthorized = false

           exporter = new Exporter options
           if opts.json
@@ -333,7 +346,11 @@
             #     {level: 'warn', stream: process.stdout}
             #   ]

-          options.host = program.sphereHost if program.sphereHost
+          options.client.host = program.sphereHost if program.sphereHost
+          if program.sphereAuthHost
+            options.client.oauth_host = program.sphereAuthHost
+            options.client.rejectUnauthorized = false
+

           # if program.verbose
           #   options.logConfig.streams = [


Rename MockFunction.setup() and cleanup() to setup_environment() and cleanup_environment()

--- src/TinyMock.coffee
+++ src/TinyMock.coffee
@@ -6,12 +6,12 @@
   @mock: (args...) ->
     mock = new MockFunction()
     mock.load_args(args)
-    mock.setup()
+    mock.setup_environment()
     try
       mock.run_test_function()
       mock.verify_expectations()
     finally
-      mock.cleanup()
+      mock.cleanup_environment()

   constructor: ->
     @_test_function = undefined
@@ -25,7 +25,7 @@
     @parse_args(args)
     @check_expects_method_name()

-  setup: (args) ->
+  setup_environment: (args) ->
     @create_mock_objects()
     @create_empty_mock_methods_list()
     @install_expects_method()
@@ -37,7 +37,7 @@
     errors = @_mock_methods.find_errors()
     fail( errors.join("\n") + "\n" ) if errors.length > 0

-  cleanup: ->
+  cleanup_environment: ->
     @_mock_methods.restore_original_methods()
     @uninstall_expects_method()


Fixed bug where syntax errors result in stack dump instead of correct output.

--- src/coffeelint.coffee
+++ src/coffeelint.coffee
@@ -171,27 +171,36 @@

     # Do AST linting first so all compile errors are caught.
     astErrors = new ASTLinter(source, config, _rules, CoffeeScript).lint()
-
-    # Do lexical linting.
-    lexicalLinter = new LexicalLinter(source, config, _rules, CoffeeScript)
-    lexErrors = lexicalLinter.lint()
-
-    # Do line linting.
-    tokensByLine = lexicalLinter.tokensByLine
-    lineLinter = new LineLinter(source, config, _rules, tokensByLine)
-    lineErrors = lineLinter.lint()
+    errors = [].concat(astErrors)
+
+    # only do further checks if the syntax is okay, otherwise they just fail
+    # with syntax error exceptions
+    if astErrors.length == 0
+        # Do lexical linting.
+        lexicalLinter = new LexicalLinter(source, config, _rules, CoffeeScript)
+        lexErrors = lexicalLinter.lint()
+        errors = errors.concat(lexErrors)
+
+        # Do line linting.
+        tokensByLine = lexicalLinter.tokensByLine
+        lineLinter = new LineLinter(source, config, _rules, tokensByLine)
+        lineErrors = lineLinter.lint()
+        errors = errors.concat(lineErrors)
+        block_config = lineLinter.block_config
+    else
+        # default this so it knows what to do
+        block_config =
+            enable : {}
+            disable : {}

     # Sort by line number and return.
-    errors = lexErrors.concat(lineErrors, astErrors)
     errors.sort((a, b) -> a.lineNumber - b.lineNumber)
-

     # Disable/enable rules for inline blocks
     all_errors = errors
     errors = []
     disabled = disabled_initially
     next_line = 0
-    block_config = lineLinter.block_config
     for i in [0...source.split('\n').length]
         for cmd of block_config
             rules = block_config[cmd][i]


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/animated.coffee
+++ src/animated.coffee
@@ -28,14 +28,14 @@
         }]
       ]
       animations: [{
-         name: ""
-         complete: ""
-         interruptible: false
-         speed: ""
-         triggers: {
-           "0": [""]
-         }
-         frames: [0]
+        name: ""
+        complete: ""
+        interruptible: false
+        speed: ""
+        triggers: {
+          "0": []
+        }
+        frames: [0]
       }]
     spriteLookup: {}
     activeAnimation:
@@ -144,14 +144,14 @@

         if updateFrame = (time - I.lastUpdate) >= I.activeAnimation.speed
           I.lastUpdate = time
-          if I.activeAnimation.triggers?[I.currentFrameIndex]
-            I.activeAnimation.triggers[I.currentFrameIndex].each (event) ->
+          if triggers = I.activeAnimation.triggers?[I.activeAnimation.frames.indexOf(I.currentFrameIndex)]
+            triggers.each (event) ->
               self.trigger(event)

           advanceFrame()
       else
-        if I.activeAnimation.triggers?[I.currentFrameIndex]
-          I.activeAnimation.triggers[I.currentFrameIndex].each (event) ->
+        if triggers = I.activeAnimation.triggers?[I.activeAnimation.frames.indexOf(I.currentFrameIndex)]
+          triggers.each (event) ->
             self.trigger(event)

         advanceFrame()


Fix OVH description

Fix OVH description

--- client/app/locales/fr.coffee
+++ client/app/locales/fr.coffee
@@ -81,31 +81,31 @@
     'konnector description online_net': "Téléchargez toutes vos factures Online.net. Pour pouvoir stocker les factures au format PDF, ce connecteur requiert que l'application Files soit installée sur votre Cozy."
     'konnector description ovh_eu': """
     Téléchargez toutes vos factures OVH Europe. Pour pouvoir stocker les factures au format PDF, ce connecteur requiert que l'application Files soit installée sur votre Cozy.

-    Lors de votre premier import nous générerons un lien à partir dulequel vous pourrez rentre vos identifiants OVH Europe.
+    Lors de votre premier import, nous générerons un lien à partir duquel vous pourrez rentrer vos identifiants OVH Europe.
     """
     'konnector description ovh_ca': """
     Téléchargez toutes vos factures OVH North-America. Pour pouvoir stocker les factures au format PDF, ce connecteur requiert que l'application Files soit installée sur votre Cozy.

-    Lors de votre premier import nous générerons un lien à partir dulequel vous pourrez rentre vos identifiants OVH North-America.
+    Lors de votre premier import, nous générerons un lien à partir duquel vous pourrez rentrer vos identifiants OVH Europe.
     """
     'konnector description runabove': """
     Téléchargez toutes vos factures RunAbove. Pour pouvoir stocker les factures au format PDF, ce connecteur requiert que l'application Files soit installée sur votre Cozy.

-    Lors de votre premier import nous générerons un lien à partir dulequel vous pourrez rentre vos identifiants RunAbove.
+    Lors de votre premier import, nous générerons un lien à partir duquel vous pourrez rentrer vos identifiants OVH Europe.
     """
     'konnector description kimsufi_eu': """
     Téléchargez toutes vos factures Kimsufi Europe. Pour pouvoir stocker les factures au format PDF, ce connecteur requiert que l'application Files soit installée sur votre Cozy.

-    Lors de votre premier import nous générerons un lien à partir dulequel vous pourrez rentre vos identifiants Kimsufi Europe.
+    Lors de votre premier import, nous générerons un lien à partir duquel vous pourrez rentrer vos identifiants OVH Europe.
     """
     'konnector description kimsufi_ca': """
     Téléchargez toutes vos factures Kimsufi North-America. Pour pouvoir stocker les factures au format PDF, ce connecteur requiert que l'application Files soit installée sur votre Cozy.

-    Lors de votre premier import nous générerons un lien à partir dulequel vous pourrez rentre vos identifiants Kimsufi North-America.
+    Lors de votre premier import, nous générerons un lien à partir duquel vous pourrez rentrer vos identifiants OVH Europe.
     """
     'konnector description soyoustart_eu': """
     Téléchargez toutes vos factures SoYouStart Europe. Pour pouvoir stocker les factures au format PDF, ce connecteur requiert que l'application Files soit installée sur votre Cozy.

-    Lors de votre premier import nous générerons un lien à partir dulequel vous pourrez rentre vos identifiants SoYouStart Europe.
+    Lors de votre premier import, nous générerons un lien à partir duquel vous pourrez rentrer vos identifiants OVH Europe.
     """
     'konnector description soyoustart_ca': """
     Téléchargez toutes vos factures SoYouStart North-America. Pour pouvoir stocker les factures au format PDF, ce connecteur requiert que l'application Files soit installée sur votre Cozy.

-    Lors de votre premier import nous générerons un lien à partir dulequel vous pourrez rentre vos identifiants SoYouStart North-America.
+    Lors de votre premier import, nous générerons un lien à partir duquel vous pourrez rentrer vos identifiants OVH Europe.
     """
     'konnector description nest': "Enregistrez la température actuelle mesurée par votre Nest."
     'konnector description isen': "Les étudiants de l'école d'ingénieur ISEN peuvent importer leurs supports de cours et leur agenda."


fix(onAddCalendar): modified the success function

--- client/app/views/menu.coffee
+++ client/app/views/menu.coffee
@@ -27,11 +27,13 @@
                 place: ''
                 tags: [t("new calendar")]
         # saving the event, creating calendar
-        calendarEvent.save {}
-        #a timeout is needed for the created calendar to appear on bottom of the menu items
-        setTimeout ->
-            $('#menuitems li.tagmenuitem:last-of-type .calendar-rename').trigger("click")
-        ,100
+        calendarEvent.save null,
+            wait: true
+            success: =>
+                #a timeout is needed for the created calendar to appear on bottom of the menu items
+                setTimeout ->
+                    $('#menuitems li.tagmenuitem:last-of-type .calendar-rename').trigger("click")
+                , 100

     activate: (href) ->
         @$('.active').removeClass 'active'