Refs #55; Added basic bar chart padding options.

--- coffee/basic/bar.coffee
+++ coffee/basic/bar.coffee
@@ -3,15 +3,18 @@
   defaults =
     style: 'grouped'
     orientation: 'horizontal'
+    padding:
+      bar: 0.08
+      group: 0.1

-  vertical_defaults =
-    style: 'grouped'
-    orientation: 'horizontal'
+  vertical_specific =
     tickFormats:
       top: Epoch.Formats.si
       bottom: Epoch.Formats.si
       left: Epoch.Formats.regular
       right: Epoch.Formats.regular
+
+  vertical_defaults = Epoch.Util.defaults(vertical_specific, defaults)

   constructor: (@options={}) ->
     if @options.orientation == 'vertical'
@@ -25,7 +28,7 @@
     if @options.orientation == 'horizontal'
       d3.scale.ordinal()
         .domain(Epoch.Util.domain(@data))
-        .rangeRoundBands([0, @innerWidth()], .1)
+        .rangeRoundBands([0, @innerWidth()], @options.padding.group)
     else
       extent = @extent((d) -> d.y)
       extent[0] = Math.min(0, extent[0])
@@ -37,7 +40,7 @@
   x1: (x0) ->
     d3.scale.ordinal()
       .domain((layer.category for layer in @data))
-      .rangeRoundBands([0, x0.rangeBand()], .08)
+      .rangeRoundBands([0, x0.rangeBand()], @options.padding.bar)

   # @return [Function] The y scale used to render the bar chart.
   y: ->
@@ -50,13 +53,13 @@
     else
       d3.scale.ordinal()
         .domain(Epoch.Util.domain(@data))
-        .rangeRoundBands([0, @innerHeight()], .1)
+        .rangeRoundBands([0, @innerHeight()], @options.padding.group)

   # @return [Function] The x scale used to render the vertical bar chart.
   y1: (y0) ->
     d3.scale.ordinal()
       .domain((layer.category for layer in @data))
-      .rangeRoundBands([0, y0.rangeBand()], .08)
+      .rangeRoundBands([0, y0.rangeBand()], @options.padding.bar)

   # Remaps the bar chart data into a form that is easier to display.
   # @return [Array] The reorganized data.


Fix online status publication

Fix online status publication

--- jarkle/server/publications.coffee
+++ jarkle/server/publications.coffee
@@ -1,3 +1,3 @@
 Meteor.publish 'userStatus', ->
-  Meteor.users.find 'profile.online': true
+  Meteor.users.find 'status.online': true


socket manage minor renaming

socket manage minor renaming

git-svn-id: e5dc7ba9c51ef2c9d08aa2e062027c8b84c81d3a@399 b2333513-9f29-43eb-bce0-aa7d4792c706

--- src/sm-redis-mediator.coffee
+++ src/sm-redis-mediator.coffee
@@ -29,7 +29,6 @@
         @pub = opts.pub
         @sub = opts.sub
         @channel = opts.channel or 'mediator'
-        @name = opts.name

         socket = opts.socket
         host = opts.host or '127.0.0.1'


Rename readTarStream() to listTarStream()

--- src/ls-archive.coffee
+++ src/ls-archive.coffee
@@ -49,14 +49,14 @@
   fileStream.on 'error', callback
   gzipStream = fileStream.pipe(zlib.createGunzip())
   gzipStream.on 'error', callback
-  readTarStream(gzipStream, callback)
+  listTarStream(gzipStream, callback)

 listTar = (archivePath, callback) ->
   fileStream = fs.createReadStream(archivePath)
   fileStream.on 'error', callback
-  readTarStream(fileStream, callback)
+  listTarStream(fileStream, callback)

-readTarStream = (inputStream, callback) ->
+listTarStream = (inputStream, callback) ->
   paths = []
   tarStream = inputStream.pipe(require('tar').Parse())
   tarStream.on 'error', callback


faster to install tool via zip not clone; so do that

--- client/code/model/tool.coffee
+++ client/code/model/tool.coffee
@@ -7,10 +7,16 @@
       login = login.replace '\n', ''
       callback "https://#{login}@github.com/scraperwiki/#{@get 'name'}-tool.git"

+  zip_url: (callback) ->
+    $.get '/tpl/github_login', (login) =>
+      login = login.replace '\n', ''
+      callback "https://#{login}@github.com/scraperwiki/#{@get 'name'}-tool/archive/master.zip"
+
   install: (callback) ->
     @_create_box().success =>
-      @git_url (url) =>
-        @exec("cd; git clone #{url} #{@get 'name'}").success callback
+      @zip_url (url) =>
+        n = @get 'name'
+        @exec("cd; curl -L -O #{url}; unzip master.zip; mv #{n}-tool-master #{n}").success callback

   setup: (callback) ->
     @exec("cd;~/#{@get 'name'}/setup").success callback


removed pointless try statement

removed pointless try statement

--- src/mist-build.coffee
+++ src/mist-build.coffee
@@ -20,16 +20,13 @@

 config.parse process.argv

-try
-  mistfile = Mistfile.find()
-  if not mistfile
-    throw 'Mistfile not found (reached filesystem boundary)'
-  mistdir = path.dirname mistfile
+mistfile = Mistfile.find()
+if not mistfile
+  throw 'Mistfile not found (reached filesystem boundary)'
+mistdir = path.dirname mistfile

-  console.log 'mist: evaporating Mistfile:', mistfile
+console.log 'mist: evaporating Mistfile:', mistfile

-  mist = Mistfile.fromFile mistfile
-  resolver = mist.resolve mistdir
-  NinjaRenderer.run resolver, null, ninjaProc
-catch e
-  throw e
+mist = Mistfile.fromFile mistfile
+resolver = mist.resolve mistdir
+NinjaRenderer.run resolver, null, ninjaProc


Support native keybndings in available package view

--- lib/available-package-view.coffee
+++ lib/available-package-view.coffee
@@ -9,8 +9,8 @@
       @div class: 'thumbnail text', =>
         @div class: 'caption', =>
           @span outlet: 'status', class: 'package-status icon'
-          @h4 class: 'package-name', _.undasherize(_.uncamelcase(name))
-          @p class: 'description', description ? ''
+          @h4 class: 'package-name native-key-bindings', tabindex: -1, _.undasherize(_.uncamelcase(name))
+          @p class: 'description native-key-bindings', tabindex: -1, description ? ''
           @div class: 'btn-toolbar', =>
             @button outlet: 'installButton', class: 'btn btn-primary', 'Install'
             @button outlet: 'learnMoreButton', class: 'btn btn-default', 'Learn More'


adding ´Open CHANGELOG` button

adding ´Open CHANGELOG` button

--- lib/installed-package-view.coffee
+++ lib/installed-package-view.coffee
@@ -36,6 +36,7 @@
         @button outlet: 'uninstallButton', class: 'btn btn-default icon icon-trashcan', 'Uninstall'
         @button outlet: 'issueButton', class: 'btn btn-default icon icon-bug', 'Report Issue'
         @button outlet: 'readmeButton', class: 'btn btn-default icon icon-book', 'Open README'
+        @button outlet: 'changelogButton', class: 'btn btn-default icon icon-squirrel', 'Open CHANGELOG'
         @button outlet: 'openButton', class: 'btn btn-default icon icon-link-external', 'Open in Atom'

       @div outlet: 'errors'
@@ -107,6 +108,15 @@
           break
       false

+    @changelogButton.on 'click', =>
+      for child in fs.listSync(@pack.path)
+        extension = path.extname(child)
+        name = path.basename(child, extension)
+        if name.toLowerCase() is 'changelog'
+          atom.workspaceView.open(child)
+          break
+      false
+
     @openButton.on 'click', =>
       atom.open(pathsToOpen: [@pack.path]) if fs.existsSync(@pack.path)
       false


Fixes issue of opening file with `Git status` command

--- lib/views/status-list-view.coffee
+++ lib/views/status-list-view.coffee
@@ -1,3 +1,4 @@
+{GitRepository} = require 'atom'
 {$$, SelectListView} = require 'atom-space-pen-views'

 git = require '../git'
@@ -55,4 +56,6 @@
       git.add file: path
     else
       openFile = confirm("Open #{path}?")
-      if openFile then atom.workspace.open(path) else GitDiff(file: path)
+      repo = GitRepository.open(atom.workspace.getActiveEditor()?.getPath())
+      fullPath = repo.getWorkingDirectory() + '/' + path
+      if openFile then atom.workspace.open(fullPath) else GitDiff(file: path)


Translated TIE/fo, T-70 and Gozanti cards

After some time away from X-wing, it's time to keep these translations up to date :P

-Added translated names for T-70, TIE/fo and Gozanti expansions
-Translated all the pilots and new cards from the expansions above
-Added 'ship' tag to both TIE Defender titles*

*Title slot is missing in TIE Defenders in Spanish. I can't remember if we needed to do something else besides adding the 'ship' tag with the translated name to fix it. If so, feel free to do it :P

--- coffeescripts/cards-es.coffee
+++ coffeescripts/cards-es.coffee
@@ -67,6 +67,9 @@
         "Kihraxz Fighter Expansion Pack": "Pack de Expansión Caza Kihraxz"
         "Hound's Tooth  Expansion Pack": "Pack de Expansión Diente de Perro"
         "The Force Awakens Core Set": "Caja Básica El Despertar de la Fuerza"
+        "T-70 X-Wing Expansion Pack": "Pack de Expansión T-70 Ala-X"
+        "TIE/fo Fighter Expansion Pack": "Pack de Expansión Caza TIE/fo"
+        "Imperial Assault Carrier Expansion Pack": "Pack de Expansión Portacazas de Asalto Imperial"
     ui:
         shipSelectorPlaceholder: "Selecciona una nave"
         pilotSelectorPlaceholder: "Selecciona un piloto"
@@ -611,8 +614,9 @@
         'Gozanti-class Cruiser':
             text: """After you execute a maneuver, you may deploy up to 2 attached ships."""
         '"Scourge"':
+            name: "Azote"
             ship: "Caza TIE"
-            text: """When attacking a defender that has 1 or more Damage cards, roll 1 additional attack die."""
+            text: """Cuando ataques a un defensor que tiene 1 o más cartas de Daño, tira 1 dado de ataque adicional."""
         "The Inquisitor":
             text: """When attacking with your primary weapon at Range 2-3, treat the range of the attack as Range 1."""
         "Zuckuss":
@@ -653,8 +657,9 @@
             name: "Novato del Esc. Azul"
             ship: "T-70 Ala-X"
         '"Red Ace"':
+            name: "As Rojo"
             ship: "T-70 Ala-X"
-            text: '''The first time you remove a shield token from your ship each round, assign 1 evade token to your ship.'''
+            text: '''La primera vez que quites una ficha de Escudos de tu nave en cada ronda, asigna 1 ficha de Evasión a tu nave.'''
         # TIE/fo
         '"Omega Ace"':
             name: '"As Omega"'
@@ -678,26 +683,31 @@
             name: "Piloto del Esc. Epsilon"
             ship: "Caza TIE/fo"
         '"Omega Leader"':
+            name: "Jefe Omega"
             ship: "Caza TIE/fo"
-            text: '''Enemy ships that you have locked cannot modify any dice when attacking you or defending against your attacks.'''
+            text: '''Las naves enemigas que tienes fijadas como blanco no pueden modificar ningún dado cuando te atacan o se defienden de tus ataques.'''
         'Hera Syndulla':
             text: '''When you reveal a green or red maneuver, you may rotate your dial to another maneuver of the same difficulty.'''
         '"Youngster"':
+            name: "Pipiolo"
             ship: "Caza TIE"
-            text: """Friendly TIE fighters at Range 1-3 may perform the action on your equipped %ELITE% Upgrade card."""
+            text: """Los Cazas TIE aliados que tengas a alcance 1-3 pueden realizar la acción de tu carta de Mejora %ELITE% equipada."""
         '"Wampa"':
             ship: "Caza TIE"
-            text: """When attacking, you may cancel all die results.  If you cancel a %CRIT% result, deal 1 facedown Damage card to the defender."""
+            text: """Cuando ataques, puedes anular todos los resultados de los dados. Si anulas al menos un resultado %CRIT%, inflinge 1 carta de Daño boca abajo al defensor."""
         '"Chaser"':
+            name: "Perseguidor"
             ship: "Caza TIE"
-            text: """When another friendly ship at Range 1 spends a focus token, assign a focus token to your ship."""
+            text: """Cuando otra nave aliada que tengas a alcance 1 gaste una ficha de Concentración, asigna 1 ficha de Concentración a tu nave."""
         'Ezra Bridger':
             text: """When defending, if you are stressed, you may change up to 2 of your %FOCUS% results to %EVADE% results."""
         '"Zeta Leader"':
-            text: '''When attacking, if you are not stressed, you may receive 1 stress token to roll 1 additional die.'''
+            name: "Jefe Zeta"
+            text: '''Cuando ataques, si no estás bajo tensión, puedes recibir 1 ficha de Tensión para tirar 1 dado de ataque adicional.'''
             ship: "Caza TIE/fo"
         '"Epsilon Ace"':
-            text: '''While you do not have any Damage cards, treat your pilot skill value as "12."'''
+            name: "As Epsilon"
+            text: '''Mientras no tengas ninguna carta de Daño asignada, se considera que tienes Habilidad 12.'''
             ship: "Caza TIE/fo"
         "Kanan Jarrus":
             text: """When an enemy ship at Range 1-2 is attacking, you may spend a focus token.  If you do, the attacker rolls 1 fewer attack die."""
@@ -713,7 +723,7 @@
             text: '''Once per round, after you discard an %ELITE% Upgrade card, flip that card faceup.'''
             ship: "Bombardero TIE"
         'Ello Asty':
-            text: '''While you are not stressed, you may treat your %TROLLLEFT% and %TROLLRIGHT% maneuvers as white maneuvers.'''
+            text: '''Mientras no estés bajo tensión, puedes ejecutar tus maniobras %TROLLLEFT% y %TROLLRIGHT% como maniobras blancas.'''
             ship: "T-70 Ala-X"
         "Valen Rudor":
             text: """After defending, you may perform a free action."""
@@ -1121,7 +1131,8 @@
             name: "Misiles Rastreadores Avanzados"
             text: """Ataque (Blanco fijado): Descarta esta carta para efectuar este ataque.%LINEBREAK%Si el ataque impacta, inflinge 1 carta de Daño boca arriba al defensor. Luego se anulan todos los resultados de los dados."""
         'Agent Kallus':
-            text: '''%IMPERIALONLY%%LINEBREAK%At the start of the first round, choose 1 enemy small or large ship.  When attacking or defending against that ship, you may change 1 of your %FOCUS% results to a %HIT% or %EVADE% result.'''
+            name: "Agente Kallus"
+            text: '''%IMPERIALONLY%%LINEBREAK%Al comienzo de la primera ronda, elige 1 nave enemiga pequeña o grande. Cuando ataques a esa nave o te defiendas de esa nave, puedes cambiar 1 de tus resultados %FOCUS% por un resultado %HIT% o %EVADE%.'''
         'XX-23 S-Thread Tracers':
             text: """Attack (focus): Discard this card to perform this attack.  If this attack hits, each friendly ship at Range 1-2 of you may acquire a target lock on the defender.  Then cancel all dice results."""
         "Tractor Beam":
@@ -1156,11 +1167,14 @@
             name: "Enardecido"
             text: """Cuando ataques o te defiendas, si estás bajo tensión, puedes volver a tirar 1 o más de tus resultados %FOCUS%."""
         'Cool Hand':
-            text: '''When you receive a stress token, you may discard this card to assign 1 focus or evade token to your ship.'''
+            name: "Mano Firme"
+            text: '''Cuando recibas una ficha de Tensión, puedes descartar esta carta para asignar 1 ficha de Concetración o de Evasión a tu nave.'''
         'Juke':
-            text: '''%SMALLSHIPONLY%%LINEBREAK%When attacking, if you have an evade token, you may change 1 of the defender's %EVADE% results into a %FOCUS% result.'''
+            name: "Finta"
+            text: '''%SMALLSHIPONLY%%LINEBREAK%Cuando ataques, si tienes una ficha de Evasión, puedes cambiar 1 de los resultados %EVADE% del defensor por un resultado %FOCUS%.'''
         'Comm Relay':
-            text: '''You cannot have more than 1 evade token.%LINEBREAK%During the End phase, do not remove an unused evade token from your ship.'''
+            name: "Repetidor de Comunicaciones"
+            text: '''No puedes tener más de 1 ficha de Evasión.%LINEBREAK%Durante la fase Final, no retires de tu nave las fichas de Evasión que no hayas usado.'''
         'Dual Laser Turret':
             text: '''%GOZANTIONLY%%LINEBREAK%Attack (energy): Spend 1 energy from this card to perform this attack against 1 ship (even a ship outside your firing arc).'''
         'Broadcast Array':
@@ -1180,7 +1194,8 @@
         'Dorsal Turret':
             text: """Attack: Attack 1 ship (even a ship outside your firing arc).%LINEBREAK%If the target of this attack is at Range 1, roll 1 additional attack die."""
         'Targeting Astromech':
-            text: '''After you execute a red maneuver, you may acquire a target lock.'''
+            name: "Droide Astromecánico de Selección de Blancos"
+            text: '''Después de que ejecutes una maniobra roja, puedes fijar un blanco.'''
         'Hera Syndulla':
             text: """%REBELONLY%%LINEBREAK%You may reveal and execute red maneuvers even while you are stressed."""
         'Ezra Bridger':
@@ -1277,7 +1292,8 @@
             name: "Motor Sublumínico Avanzado"
             text: """Después de efectuar una acción de MASA, si no te has solapado con un obstáculo ni con otra nave, puedes llevar a cabo una acctión gratuita."""
         'Integrated Astromech':
-            text: '''X-wing only.%LINEBREAK%When you are dealt a Damage card, you may discard 1 of your %ASTROMECH% Upgrade cards to discard that Damage card.'''
+            name: "Droide Astromecánico Integrado"
+            text: '''Solo X-wing.%LINEBREAK%Cuando recibas una carta de Daño, puedes descartar 1 de tus cartas de Mejora %ASTROMECH% para descartar esa carta de Daño (sin resolver su efecto).'''
         'Optimized Generators':
             text: '''%HUGESHIPONLY%%LINEBREAK%Once per round, when you assign energy to an equipped Upgrade card, gain 2 energy.'''
         'Automated Protocols':
@@ -1394,8 +1410,10 @@
             text: """Sólo sección de popa de corbeta clase Incursor.%LINEBREAK%Después de que hayas efectuado un ataque que destruya una nave enemiga, puedes fijar un blanco."""
         'TIE/x7':
             text: '''TIE Defender only.%LINEBREAK%Your upgrade bar loses the %CANNON% and %MISSILE% upgrade icons.%LINEBREAK%After executing a 3-, 4-, or 5-speed maneuver, you may assign 1 evade token to your ship.'''
+            ship: 'Defensor TIE'
         'TIE/D':
             text: '''TIE Defender only.%LINEBREAK%Once per round, after you perform an attack with a %CANNON% secondary weapon that costs 3 or fewer squad points, you may perform a primary weapon attack.'''
+            ship: 'Defensor TIE'
         'TIE Shuttle':
             text: '''TIE Bomber only.%LINEBREAK%Your upgrade bar loses all %TORPEDO%, %MISSILE%, and %BOMB% upgrade icons and gains 2 %CREW% upgrade icons.  You cannot equip a %CREW% Upgrade card that costs more than 4 squad points.'''
         'Requiem':


Add (untranslated) Scum titles for ES translations.

fixes #189

--- coffeescripts/cards-es.coffee
+++ coffeescripts/cards-es.coffee
@@ -1033,5 +1033,26 @@
             text: """Solo Firespray-31.

Tu barra de mejoras gana 2 iconos %BOMB% adicionales."""
         "TIE/x1":
             text: """TIE Advanced only.%LINEBREAK%Your upgrade bar gains the %SYSTEM% upgrade icon.%LINEBREAK%If you equip a %SYSTEM% upgrade, its squad point cost is reduced by 4 (to a minimum of 0)."""
+        "BTL-A4 Y-Wing":
+            name: "BTL-A4 Y-Wing"
+            ship: "Ala-Y"
+        "IG-2000":
+            name: "IG-2000"
+            ship: "Agresor"
+        "Virago":
+            name: "Virago"
+            ship: 'Víbora Estelar'
+        '"Heavy Scyk" Interceptor (Cannon)':
+            name: '"Heavy Scyk" Interceptor (Cannon)'
+            ship: 'Interceptor M3-A'
+        '"Heavy Scyk" Interceptor (Missile)':
+            name: '"Heavy Scyk" Interceptor (Missile)'
+            ship: 'Interceptor M3-A'
+        '"Heavy Scyk" Interceptor (Torpedo)':
+            name: '"Heavy Scyk" Interceptor (Torpedo)'
+            ship: 'Interceptor M3-A'
+        "Dauntless":
+            name: "Dauntless"
+            ship: 'VT-49 Diezmador'

     exportObj.setupCardData basic_cards, pilot_translations, upgrade_translations, modification_translations, title_translations


Handle collection updates and logout properly.

--- coffeescripts/manifest.coffee
+++ coffeescripts/manifest.coffee
@@ -2543,10 +2543,10 @@
         # To save collection (optional)
         @backend = args.backend

-        @reset()
-
         @setupUI()
         @setupHandlers()
+
+        @reset()

         @language = 'English'

@@ -2562,6 +2562,27 @@
                 for card in (exportObj.manifestByExpansion[expansion] ? [])
                     for _ in [0...card.count]
                         ((@shelf[card.type] ?= {})[card.name] ?= []).push expansion
+
+        @counts = {}
+        for own type of @shelf
+            for own thing of @shelf[type]
+                (@counts[type] ?= {})[thing] ?= 0
+                @counts[type][thing] += @shelf[type][thing].length
+
+        component_content = $ @modal.find('.collection-inventory-content')
+        component_content.text ''
+        for own type, things of @counts
+            contents = component_content.append $.trim """
+

+

#{type.capitalize()}

+

+

+

+

+            """
+            ul = $ contents.find("ul#counts-#{type}")
+            for thing in Object.keys(things).sort(sortWithoutQuotes)
+                ul.append """
#{thing} - #{things[thing]}
"""

     fixName: (name) ->
         # Special case handling for Heavy Scyk :(
@@ -2657,25 +2678,6 @@
             $(row).find('.expansion-name').data 'english_name', expansion
             collection_content.append row

-        component_content = $ @modal.find('.collection-inventory-content')
-        @counts = {}
-        for own type of @shelf
-            for own thing of @shelf[type]
-                (@counts[type] ?= {})[thing] ?= 0
-                @counts[type][thing] += @shelf[type][thing].length
-        for own type, things of @counts
-            contents = component_content.append $.trim """
-

-

#{type.capitalize()}

-

-

-

-

-            """
-            ul = $ contents.find("ul#counts-#{type}")
-            for thing in Object.keys(things).sort(sortWithoutQuotes)
-                ul.append """
#{thing} - #{things[thing]}
"""
-
     destroyUI: ->
         @modal.modal 'hide'
         @modal.remove()


Do not add undefined if class not provided

--- core/KDView.coffee
+++ core/KDView.coffee
@@ -644,7 +644,7 @@
     cssClass    ?= "transparent"
     parent      ?= "body"           #body or a KDView instance

-    @$overlay = $ "<div />", class : "kdoverlay #{cssClass} #{if animated then "animated"}"
+    @$overlay = $ "<div />", class : "kdoverlay #{cssClass}#{if animated then " animated" else ''}"

     if color
       @$overlay.css "background-color" : color


fixed Shift key event

fixed Shift key event

--- app/coffeescripts/AppView.coffee
+++ app/coffeescripts/AppView.coffee
@@ -17,7 +17,8 @@
     @_layersMenu()
     @_messagePanel()
     self = @
-    google.maps.event.addDomListener(window, 'keydown', (event) -> self._setKeyEvents(event))
+    google.maps.event.addDomListener(window, 'keydown', (event) -> self._setKeyDownEvents(event))
+    google.maps.event.addDomListener(window, 'keyup', (event) -> self._setKeyUpEvents(event))
     $a.broker.on('map:upload_complete', @_displayMap, @)
     $a.broker.on("app:clear_map", @clearMap, @)
     @
@@ -71,20 +72,23 @@
     new $a.MapNetworkModel()
     @mapView = new $a.MapNetworkView $a.models

-  _setKeyEvents: (e) ->
+  _setKeyDownEvents: (e) =>
     # Open Local Network ALT-A
-    if e.type == 'keydown' and $a.ALT_DOWN and e.keyCode == 65
+    if $a.ALT_DOWN and e.keyCode == 65
       @clearMap()
       $("#uploadField").click()

     # Set multi-select of map elements with the shift key
-    $a.SHIFT_DOWN = false
-    $a.SHIFT_DOWN = true if e.type == 'keydown' and e.keyCode == 16
+    $a.SHIFT_DOWN = true if e.keyCode == 16

-    # Set multi-select of map elements with the shift key
-    $a.ALT_DOWN = false
-    $a.ALT_DOWN = true if e.type == 'keydown' and e.keyCode == 18
-
+    # Set alt key down in order to set up quick key for opening files
+    $a.ALT_DOWN = true if e.keyCode == 18
+
+  _setKeyUpEvents: (e) =>
+    # Turn off shift and alt down flags where appropriate
+    $a.SHIFT_DOWN = false if e.keyCode == 16
+    $a.ALT_DOWN = false  if e.keyCode == 18
+
   clearMap: =>
     $a.broker.trigger('map:clear_map')
     $a.broker.trigger('app:tree_clear')


fix: remove canvas

fix: remove canvas

--- app/components/vnc-viewer.coffee
+++ app/components/vnc-viewer.coffee
@@ -98,6 +98,8 @@
       rfb = @get "rfb"
       if rfb._rfb_connection_state is 'connected'
         rfb.disconnect()
+        oldCanvas = @element.getElementsByClassName("canvas")[0]
+        oldCanvas.remove()

 `export default VncViewerComponent`


updated deploy module

updated deploy module

--- core/deploy.coffee
+++ core/deploy.coffee
@@ -40,6 +40,38 @@
     else
         return Path.join(__dirname,"..")

+initLocationDataCopy  = (mew,location,config,callback)->
+    if config.datas and Array.isArray(config.datas)
+        dataCopyCallback = ->
+            makeLocationDir(location,"/var/data")
+            data = config.datas.shift()
+            if data
+                dataFile = mew.getDataFile(data)
+                Fs.exists dataFile,(exists)->
+                    if exists
+                        Fs.stat dataFile,(err,stat)->
+                            if err
+                                callback(err)
+                            else
+                                if stat.isDirectory()
+                                    Fse.overwriteRecursive dataFile,makeLocationDir(location,"/var/data/#{data}"),(err)->
+                                        if err
+                                            callback(err)
+                                        else
+                                            dataCopyCallback()
+                                else
+                                    Fse.overwrite dataFile,getLocationFile(location,"/var/data/#{data}"),(err)->
+                                        if err
+                                            callback(err)
+                                        else
+                                            dataCopyCallback()
+                    else
+                        callback("data file to copy : [#{data}] doesnt exists")
+            else
+                callback()
+        dataCopyCallback()
+    else
+        callback()
 initLocationBasicHirastructor = (location,callback)->
     Fse.overwriteRecursive getSourceFile("node_modules"),makeLocationDir(location,"node_modules"),(err)->
         return callback(err) if err
@@ -164,7 +196,9 @@
                                     initLocationService location,config,(err)=>
                                         return callback(err) if err
                                         initLocationProfile location,config,(err)=>
-                                            callback(err)
+                                            return callback(err) if err
+                                            initLocationDataCopy @mew,location,config,(err)=>
+                                                callback(err)
                         else
                             callback("location is not a directory")
         else


Update Eve Gaus

Update Eve Gaus

--- app/controllers/team-page.coffee
+++ app/controllers/team-page.coffee
@@ -7,9 +7,9 @@

The Field Museum

Matt von Konrat, Principal Investigator (Head of Botanical Collections, Science & Education)

+
Eve Gaus, Co-Principal Investigator (Digital Learning Manager, Education Department)

Juan Larrain (Post Doctoral Research Scholar, Science & Education)

Beth Crownover (Director, Education Department)

-
Eve Gaus (Digital Learning Manager, Education Department)

Jessica Hankey (School Partnerships and Programs Manager, Education Department)

Kristina Lugo (Annual Fund Coordinator, Institutional Advancement)

Juan Larrain (Post Doctoral Research Scholar, Science & Education)


Two spaces for css files.

Two spaces for css files.

--- env/atom.config.cson
+++ env/atom.config.cson
@@ -183,6 +183,10 @@
 ".coffee.source":
 	editor:
 		tabLength: 2
+".css.less.source":
+	editor:
+		softTabs: true
+		tabLength: 2
 ".gfm.source":
 	editor:
 		softTabs: true


Make download link for Photograph open in new tab

--- app/assets/javascripts/browser.coffee
+++ app/assets/javascripts/browser.coffee
@@ -185,10 +185,11 @@
 					row.append(photographLink)
 				# Setup Fancybox
 				$(".fancybox").fancybox({
+					padding: 0,
 					type: 'image',
 					afterLoad: ->
 						originalImageLink = @.href.split("/resize")
-						@.title = 'Download '+@.title
+						@.title = 'Download '+@.title
 				})
 				return
 			, error: (jqXHR, textStatus, errorThrown) ->


close #908 show dialog when discard the card editor with content

--- app/assets/javascripts/card.js.coffee
+++ app/assets/javascripts/card.js.coffee
@@ -102,6 +102,9 @@
     $.fancybox.open
       padding: 0
       href: "#card-form-container"
+      helpers:
+        overlay:
+          closeClick: false

   $(document).on "ajax:success", ".delete-card", (xhr, data) ->
     link = $ this
@@ -115,6 +118,12 @@
   $(document).on "click", ".cancel-btn", (event) ->
     event.preventDefault()
     $.fancybox.close()
+
+
+  $(document).on "click", ".fancybox-overlay", (event) ->
+    event.preventDefault()
+    if confirm "Are you sure to discard all changes on this dialog?"
+      $.fancybox.close()

   $(document).on "card-order-changed", "#recipe-card-list", (event) ->
     setStateIndex()


JS to fix activity being wrong size

JS to fix activity being wrong size

--- app/assets/javascripts/days.js.coffee
+++ app/assets/javascripts/days.js.coffee
@@ -15,6 +15,8 @@
     $('.remove_day').show();
     $('.remove_activity').show();
     $('.add_fields').show();
+    $(".activity.field").remove();
+    $(".activity_link.add_fields").click();

   try $('.datepicker').datepicker({
         datesDisabled: $('.datepicker').data().dateDatesDisabled.split(",")


Fix toolbar navigation for bepo

Fix toolbar navigation for bepo

--- app/assets/javascripts/toolbar.coffee
+++ app/assets/javascripts/toolbar.coffee
@@ -27,7 +27,7 @@
       $("#toolbar_items .next").click @next_item
       $("#toolbar .change").click @change_threshold
     $(document).bind("keypress", "<", @prev_item)
-               .bind("keypress", "shift+>", @next_item)
+               .bind("keypress", ">", @next_item)
                .bind("keypress", "k", @prev_item)
                .bind("keypress", "j", @next_item)


updating builder to accept dates and moments when setting due date and open date

--- src/components/task-plan/builder.cjsx
+++ src/components/task-plan/builder.cjsx
@@ -8,6 +8,7 @@
 BindStoreMixin = require '../bind-store-mixin'

 {TimeStore} = require '../../flux/time'
+TutorDateFormat = TimeStore.getFormat()
 TimeHelper = require '../../helpers/time'

 {TaskPlanStore, TaskPlanActions} = require '../../flux/task-plan'
@@ -123,10 +124,16 @@

   setOpensAt: (value, period) ->
     {id} = @props
+    if not value._isAMomentObject
+      value = moment(value).format(TutorDateFormat)
+
     TaskPlanActions.updateOpensAt(id, value, period?.id)

   setDueAt: (value, period) ->
     {id} = @props
+    if not value._isAMomentObject
+      value = moment(value).format(TutorDateFormat)
+
     TaskPlanActions.updateDueAt(id, value, period?.id)

   setAllPeriods: ->


fix #1768 split tags by comma

fix #1768 split tags by comma

--- app/coffee/modules/common/tags.coffee
+++ app/coffee/modules/common/tags.coffee
@@ -101,6 +101,7 @@

 LbTagLineDirective = ($rs) ->
     ENTER_KEY = 13
+    COMMA_KEY = 188

     template = """

@@ -163,6 +164,10 @@
             resetInput()
             hideSaveButton()

+        removeInputLastCharacter = (input) =>
+            inputValue = input.val()
+            input.val inputValue.substring(0, inputValue.length - 1)
+
         ## Events
         $el.on "keypress", "input", (event) ->
             return if event.keyCode != ENTER_KEY
@@ -172,6 +177,9 @@
             target = angular.element(event.currentTarget)

             if event.keyCode == ENTER_KEY
+                saveInputTag()
+            else if event.keyCode == COMMA_KEY
+                removeInputLastCharacter(target)
                 saveInputTag()
             else
                 if target.val().length
@@ -231,6 +239,7 @@
 TagLineDirective = ($rootScope, $repo, $rs, $confirm, $qqueue) ->
     ENTER_KEY = 13
     ESC_KEY = 27
+    COMMA_KEY = 188

     template = """

@@ -335,6 +344,10 @@
             resetInput()
             hideSaveButton()

+        removeInputLastCharacter = (input) =>
+            inputValue = input.val()
+            input.val inputValue.substring(0, inputValue.length - 1)
+
         ## Events
         $el.on "keypress", "input", (event) ->
             return if event.keyCode not in [ENTER_KEY, ESC_KEY]
@@ -344,6 +357,9 @@
             target = angular.element(event.currentTarget)

             if event.keyCode == ENTER_KEY
+                saveInputTag()
+            else if event.keyCode == COMMA_KEY
+                removeInputLastCharacter(target)
                 saveInputTag()
             else if event.keyCode == ESC_KEY
                 resetInput()


Fixed small problem with the users filters refactor

--- app/coffee/modules/issues/list.coffee
+++ app/coffee/modules/issues/list.coffee
@@ -147,7 +147,7 @@
                     else
                         obj.name = unknownOption

-                    obj.selected = true if isSelected("createdBy", obj.id)
+                    obj.selected = true if isSelected(type, obj.id)
                     return obj
                 unknownItem = _.remove(reformatedUsers, (u) -> not u.id)
                 reformatedUsers = _.sortBy(reformatedUsers, (u) -> u.name.toUpperCase())


Remove unnecessary TODO comment

Remove unnecessary TODO comment

--- app/coffee/modules/issues/list.coffee
+++ app/coffee/modules/issues/list.coffee
@@ -89,7 +89,6 @@
             @scope.project = project
             @scope.$emit('project:loaded', project)

-            # TODO: add issueTypeList and issueTypeById
             @scope.issueStatusById = groupBy(project.issue_statuses, (x) -> x.id)
             @scope.issueStatusList = _.sortBy(project.issue_statuses, "order")
             @scope.severityById = groupBy(project.severities, (x) -> x.id)


Fix requests page

Fix requests page

--- app/helpers/github-commit-link.coffee
+++ app/helpers/github-commit-link.coffee
@@ -3,9 +3,9 @@

 helper = Ember.Handlebars.makeBoundHelper (slug, commitSha) ->
   return '' unless commitSha
-  sha = Handlebars.Utils.escapeExpression formatCommit(commitSha)
+  sha = Ember.Handlebars.Utils.escapeExpression formatCommit(commitSha)
   return sha unless slug
-  url = Handlebars.Utils.escapeExpression githubCommitUrl(slug, sha)
+  url = Ember.Handlebars.Utils.escapeExpression githubCommitUrl(slug, sha)

   safe '' + sha + ''


Fixed duplicate call to /authenticate

Fixed duplicate call to /authenticate

--- app/scripts/services/auth.coffee
+++ app/scripts/services/auth.coffee
@@ -2,7 +2,6 @@

 class AuthService
   constructor: (@rootScope, @location, @restangular, @googlePlus, @localStorage, @UserViolationsPrefsFcty) ->
-    @getAuthenticationFlags()
     @bindEvents()

   bindEvents: ->


Update obsolete Atom config for JavaScript

--- modules/atom/settings/config.cson
+++ modules/atom/settings/config.cson
@@ -14,22 +14,23 @@
     disabledPackages: [
       "metrics"
       "welcome"
+      "markdown-preview"
       "language-javascript"
-      "markdown-preview"
     ]
     telemetryConsent: "no"
   editor:
     showIndentGuide: true
   "exception-reporting":
     userId: "e2c17e7a-6562-482a-b326-cd43e0f43b75"
+  "language-babel":
+    autoIndentJSX: true
   linter:
     disabledProviders: [
       "ESLint"
     ]
   "linter-eslint":
-    disableWhenNoEslintConfig: true
-    globalNodePath: "/usr/local"
-    useGlobalEslint: false
+    global:
+      globalNodePath: "/usr/local"
   "linter-ui-default":
     panelHeight: 69
   "markdown-preview":


aggregate tail chooses running tasks by default if there are any

--- SingularityUI/app/components/aggregateTail/AggregateTail.cjsx
+++ SingularityUI/app/components/aggregateTail/AggregateTail.cjsx
@@ -37,10 +37,17 @@
     else
       document.title = "Tail of #{@props.path.replace('$TASK_ID', 'Task Directory')}"

+  initialViewingInstancesOnUpdate: ->
+    runningTasks = (task for task in @state.activeTasks when task.lastTaskState == 'TASK_RUNNING')
+    if runningTasks.length > 0
+      return runningTasks
+    else
+      return @state.activeTasks
+
   componentDidUpdate: (prevProps, prevState) ->
     if prevState.activeTasks.length is 0 and @state.activeTasks.length > 0 and not Utils.getQueryParams()?.taskIds and !@props.singleMode
       @setState
-        viewingInstances: _.pluck(@state.activeTasks, 'id').slice(0, 6)
+        viewingInstances: _.pluck(@initialViewingInstancesOnUpdate(), 'id').slice(0, 6)

   componentWillUnmount: ->
     Backbone.React.Component.mixin.off(@);


revert ff6ba5, prepare for other implementation

--- src/documents/lib/js2coffee.js.coffee
+++ src/documents/lib/js2coffee.js.coffee
@@ -223,11 +223,7 @@
     if str.match(/^([_\$a-z][_\$a-z0-9]*)$/i) or str.match(/^[0-9]+$/i)
       @l(n)+str
     else
-      str = strEscape str
-      if @options.single_quotes is true
-        str = "'" + str.substr(1, str.length - 2) + "'"
-
-      @l(n)+str
+      @l(n)+strEscape str

   # `identifier`
   # Any object identifier like a variable name.
@@ -441,11 +437,7 @@
       @l(n)+"/#{value}/#{flag}"

   'string': (n) ->
-    str = strEscape n.value
-    if @options.single_quotes is true
-      str = "'" + str.substr(1, str.length - 2) + "'"
-
-    @l(n)+ str
+    @l(n)+ strEscape n.value

   # `call`
   # A Function call.


fix(composer): fix bug where message body says undefined

--- internal_packages/composer/lib/contenteditable-component.cjsx
+++ internal_packages/composer/lib/contenteditable-component.cjsx
@@ -105,5 +105,8 @@
       @props.html
     else
       quoteStart = @_htmlQuotedTextStart()
-      @props.html.substr(0, quoteStart) unless quoteStart is -1
+      if quoteStart is -1
+        return @props.html
+      else
+        return @props.html.substr(0, quoteStart)


ListView: Fixed some cases of row de-select.

- Don't pass the current event to the deselect call
  of other rows which the user did not click on. This
  fixes cases where the node rejects deselect when the
  user clicks on it, but deselects the node when
  another is selected.

--- src/elements/ListView/ListView.coffee
+++ src/elements/ListView/ListView.coffee
@@ -479,7 +479,9 @@
 			for _row in @getSelectedRows()
 				if row == _row
 					continue
-				ret = _row.deselect(ev)
+				# don't pass an event to deselect, as this
+				# is not the node where the user clicked on
+				ret = _row.deselect()
 				if isPromise(ret)
 					promises.push(ret)
 			CUI.when(promises).done(do_select).fail(dfr.reject)


ListView: Set minimum col width while resizing to 5.

--- src/elements/ListView/ListView.coffee
+++ src/elements/ListView/ListView.coffee
@@ -584,7 +584,7 @@

 	setColWidth: (col_i, width) ->
 		# CUI.debug "setColWidth", col_i, width
-		@__manualColWidths[col_i] = Math.max(20, width)
+		@__manualColWidths[col_i] = Math.max(5, width)
 		delete(@__colWidths[col_i])
 		@__doLayout(resetRows: true)
 		@


added disabled colour

added disabled colour

--- modules/palette/main/index.coffee
+++ modules/palette/main/index.coffee
@@ -1,5 +1,5 @@
-contexts = ['action', 'positive', 'negative', 'warning', 'info', 'complement', 'contrast']
-paletteContexts = ['default', 'action', 'positive', 'negative', 'warning', 'info', 'complement', 'contrast']
+contexts = ['action', 'positive', 'negative', 'warning', 'info', 'complement', 'contrast', 'disabled']
+paletteContexts = ['default', 'action', 'positive', 'negative', 'warning', 'info', 'complement', 'contrast', 'disabled']

 hx.palette = {}

@@ -27,4 +27,4 @@
 hx.palette.context = context(contexts, 'hx')
 hx.palette.textContext = context(paletteContexts, 'hx-text')
 hx.palette.backgroundContext = context(paletteContexts, 'hx-background')
-hx.palette.borderContext = context(paletteContexts, 'hx-border')+hx.palette.borderContext = context(paletteContexts, 'hx-border')


Add Atom packages

Add Atom packages

--- atom/packages.cson
+++ atom/packages.cson
@@ -10,6 +10,7 @@
   "git-history"
   "git-plus"
   "git-time-machine"
+  "highlight-selected"
   "language-elixir"
   "language-elm"
   "language-haml"
@@ -20,6 +21,7 @@
   "multi-cursor-plus"
   "package-sync"
   "pane-layout-plus"
+  "pigments"
   "rails-snippets"
   "rails-transporter"
   "react"


Atom - Add ex-mode package for vim :w command

--- atom/packages.cson
+++ atom/packages.cson
@@ -12,4 +12,5 @@
   "platformio-ide-terminal"
   "script"
   "vim-mode-plus"
+  "ex-mode"
 ]


Add teletype

Add teletype

--- atom/packages.cson
+++ atom/packages.cson
@@ -47,6 +47,7 @@
   "smart-tab-name"
   "sort-lines"
   "split-diff"
+  "teletype"
   "toggle-quotes"
   "vim-mode-plus"
 ]


Add SuiteScript 2.0 snippets

Add SuiteScript 2.0 snippets

--- atom/snippets.cson
+++ atom/snippets.cson
@@ -402,7 +402,7 @@
     'prefix': 'getthemeabsoluteurl'
     'body': 'getThemeAbsoluteUrlOfNonManagedResources($1)$2'

-  # SuiteScript
+  # SuiteScript 1.0

   'log execution':
     'prefix': 'nlapiLogExecution'
@@ -472,6 +472,24 @@
     'prefix': 'getscriptparameter'
     'body': 'getSetting(\'SCRIPT\', \'custscript_${1:paramname}\')'

+  # SuiteScript 2.0
+
+  'log.debug':
+    'prefix': 'logdebug'
+    'body': 'log.debug({title: ${1:title}, details: ${2:details}});'
+
+  'search.create':
+    'prefix': 'searchcreate'
+    'body': 'search.create({\n\ttype: \'${1:type}\'$2\n})$3'
+
+  'getValue':
+    'prefix': 'getvalue'
+    'body': 'getValue({name: \'$1\'$2})'
+
+  'getText':
+    'prefix': 'gettext'
+    'body': 'getText({name: \'$1\'$2})'
+
   # SuiteCommerce Advanced

   'AMD module':


Add useful PHP array snippets

Add useful PHP array snippets

--- atom/snippets.cson
+++ atom/snippets.cson
@@ -390,13 +390,29 @@
     'prefix': 'while'
     'body': 'while ( ${1:condition} ) {\n\t$2\n}'

-  'wrapped array':
-    'prefix': '(a)'
-    'body': '( array(\n\t$2\n) )$3'
-
-  'wrapped array (w/ semicolon)':
-    'prefix': '(a);'
-    'body': '( array(\n\t$2\n)$3 );'
+  'array (one-line; space-padded)':
+    'prefix': 'ar'
+    'body': 'array( $1 )'
+
+  'array (one-line; space-padded)':
+    'prefix': 'ars'
+    'body': ' array( $1 ) '
+
+  'array (empty)':
+    'prefix': 'are'
+    'body': 'array()'
+
+  'array (empty; space-padded)':
+    'prefix': 'ares'
+    'body': ' array() '
+
+  'array (multiline)':
+    'prefix': 'arn'
+    'body': 'array(\n\t$1\n)'
+
+  'array (multiline; space-padded)':
+    'prefix': 'arns'
+    'body': ' array(\n\t$1\n) '

   'key-value pair':
     'prefix': 'kv'


Remove tab stop label from afi snippet

Remove tab stop label from afi snippet

--- atom/snippets.cson
+++ atom/snippets.cson
@@ -195,7 +195,7 @@

   'arrow function (inline)':
     'prefix': 'afi'
-    'body': '($1) => ${2:returnvalue}'
+    'body': '($1) => $2'

   'immediately-invoked function expression':
     'prefix': 'iife'


Add addEventListener snippet

Add addEventListener snippet

--- atom/snippets.cson
+++ atom/snippets.cson
@@ -247,6 +247,10 @@
     'prefix': 'qsa'
     'body': 'querySelectorAll($1)$2'

+  'addEventListener':
+    'prefix': 'addeventlistener'
+    'body': 'addEventListener($1, $2);$3'
+
   'requestAnimationFrame':
     'prefix': 'raf'
     'body': 'requestAnimationFrame($1);$2'


Listen for uninstall events on package manager

Closes #70

--- lib/available-package-view.coffee
+++ lib/available-package-view.coffee
@@ -45,6 +45,15 @@
         @installButton.prop('disabled', true)
         @setStatusIcon('cloud-download')

+    @subscribe @packageManager, 'package-uninstalled package-uninstall-failed theme-uninstalled theme-uninstall-failed', (pack, error) =>
+      if pack.name is @pack.name
+        if error?
+          @setStatusIcon('alert')
+        else
+          @installButton.prop('disabled', false)
+          @installButton.text('Install')
+          @setStatusIcon()
+
     if atom.packages.isPackageLoaded(@pack.name)
       @installButton.prop('disabled', true)
       @installButton.text('Installed')
@@ -54,7 +63,7 @@

   setStatusIcon: (iconName) ->
     @status.removeClass('icon-check icon-alert icon-cloud-download')
-    @status.addClass("icon-#{iconName}")
+    @status.addClass("icon-#{iconName}") if iconName
     @status.destroyTooltip()
     switch iconName
       when 'check'


fixing issue #427 -- incorrect stringification of reserved word

--- lexer.coffee
+++ lexer.coffee
@@ -530,7 +530,7 @@
 STRING_NEWLINES : /\n[ \t]*/g
 NO_NEWLINE      : /^([+\*&|\/\-%=<>:!.\\][<>=&|]*|and|or|is|isnt|not|delete|typeof|instanceof)$/
 HEREDOC_INDENT  : /(\n+([ \t]*)|^([ \t]+))/g
-ASSIGNED        : /^([a-zA-Z\$_]\w*[ \t]*?[:=])/
+ASSIGNED        : /^([a-zA-Z\$_]\w*[ \t]*?[:=][^=])/
 NEXT_CHARACTER  : /^\s*(\S)/

 # Tokens which a regular expression will never immediately follow, but which


more refactors to interpolation

more refactors to interpolation

--- lexer.coffee
+++ lexer.coffee
@@ -99,7 +99,7 @@
     return false unless string:
       @balanced_token(['"', '"'], ['${', '}']) or
       @balanced_token ["'", "'"]
-    @interpolate_string string.replace(STRING_NEWLINES, " \\\n"), merge: true
+    @interpolate_string string.replace(STRING_NEWLINES, " \\\n")
     @line += count string, "\n"
     @i += string.length
     true
@@ -134,13 +134,7 @@
       str: regex.substring(1).split('/')[0]
       str: str.replace REGEX_ESCAPE, (escaped) -> '\\' + escaped
       @tokens: @tokens.concat [['(', '('], ['NEW', 'new'], ['IDENTIFIER', 'RegExp'], ['CALL_START', '(']]
-      interp_tokens: @interpolate_string "\"$str\"", merge: false
-      for each, i in interp_tokens
-        switch each[0]
-          when 'TOKENS' then @tokens: @tokens.concat each[1]
-          when 'STRING' then @token each[0], each[1].substring(0, 1) + each[1].substring(1, each[1].length - 1).replace(/"/g, '\\"') + each[1].substring(0, 1)
-          else @token each[0], each[1]
-        @token '+', '+' if i < interp_tokens.length - 1
+      @interpolate_string "\"$str\"", yes
       @tokens: @tokens.concat [[',', ','], ['STRING', "'$flags'"], [')', ')'], [')', ')']]
     else
       @token 'REGEX', regex
@@ -348,7 +342,7 @@
   # If it encounters an interpolation, this method will recursively create a
   # new Lexer, tokenize the interpolated contents, and merge them into the
   # token stream.
-  interpolate_string: (str, merge) ->
+  interpolate_string: (str, escape_quotes) ->
     if str.length < 3 or not starts str, '"'
       @token 'STRING', str
     else
@@ -380,13 +374,16 @@
         i += 1
       tokens.push ['STRING', "$quote${ str.substring(pi, i) }$quote"] if pi < i and pi < str.length - 1
       tokens.unshift ['STRING', "''"] unless tokens[0][0] is 'STRING'
-      if (merge ? true)
-        for each, i in tokens
-          if each[0] is 'TOKENS'
-            @tokens: @tokens.concat each[1]
-          else
-            @token each[0], each[1]
-          @token '+', '+' if i < tokens.length - 1
+      for token, i in tokens
+        [tag, value]: token
+        if tag is 'TOKENS'
+          @tokens: @tokens.concat value
+        else if tag is 'STRING' and escape_quotes
+          escaped: value.substring(1, value.length - 1).replace(/"/g, '\\"')
+          @token tag, "\"$escaped\""
+        else
+          @token tag, value
+        @token '+', '+' if i < tokens.length - 1
       tokens

   # Helpers


fixing heredocs with multiple double quotes (broken regex from the Ruby translation), with tests.

--- lexer.coffee
+++ lexer.coffee
@@ -159,7 +159,7 @@
     indent: (doc.match(HEREDOC_INDENT) or ['']).sort()[0]
     doc: doc.replace(new RegExp("^" + indent, 'gm'), '')
             .replace(MULTILINER, "\\n")
-            .replace('"', '\\"')
+            .replace(/"/g, '\\"')
     @token 'STRING', '"' + doc + '"'
     @line += @count match[1], "\n"
     @i += match[1].length


account: disallow angle brackets in firstName/lastName

They are ugly.  I don't want users with the "first name" e.g. <script>alert(0)</script>

--- workers/social/lib/social/models/account.coffee
+++ workers/social/lib/social/models/account.coffee
@@ -33,6 +33,8 @@
   Validators = require './group/validators'
   Protected = require '../traits/protected'
   {extend} = require 'underscore'
+
+  validateFullName = (value) -> not /<|>/.test value

   @share()

@@ -320,9 +322,11 @@
           type              : String
           required          : yes
           default           : 'a koding'
+          validate          : validateFullName
         lastName            :
           type              : String
           default           : 'user'
+          validate          : validateFullName
         description         : String
         avatar              : String
         status              : String
@@ -1076,7 +1080,7 @@
   fetchPrivateChannel:(callback)->
     require('bongo').fetchChannel @getPrivateChannelName(), callback

-  getPrivateChannelName:-> "private-#{@getAt('profile.nickname')}-private"
+  getPrivateChannelName:-> "private-#{@getAt('pro file.nickname')}-private"

   fetchMail:do ->


removed @taint call from updateFlags method

--- workers/social/lib/social/models/account.coffee
+++ workers/social/lib/social/models/account.coffee
@@ -696,7 +696,6 @@

   updateFlags: secure (client, flags, callback)->
     {delegate} = client.connection
-    JAccount.taint @getId()
     if delegate.can 'flag', this
       @update {$set: globalFlags: flags}, callback
     else


config: add integration worker to prod config as disabled

--- config/main.prod.coffee
+++ config/main.prod.coffee
@@ -539,19 +539,19 @@
       supervisord       :
         command         : "#{GOBIN}/team -c #{socialapi.configFilePath}"

-    integration         :
-      group             : "socialapi"
-      ports             :
-        incoming        : "#{integration.port}"
-      supervisord       :
-        command         : "#{GOBIN}/webhook -c #{socialapi.configFilePath}"
-      healthCheckURL    : "#{customDomain.local}/api/integration/healthCheck"
-      versionURL        : "#{customDomain.local}/api/integration/version"
-      nginx             :
-        locations       : [
-          location      : "~ /api/integration/(.*)"
-          proxyPass     : "http://integration/$1$is_args$args"
-        ]
+    #integration         :
+      #group             : "socialapi"
+      #ports             :
+        #incoming        : "#{integration.port}"
+      #supervisord       :
+        #command         : "#{GOBIN}/webhook -c #{socialapi.configFilePath}"
+      #healthCheckURL    : "#{customDomain.local}/api/integration/healthCheck"
+      #versionURL        : "#{customDomain.local}/api/integration/version"
+      #nginx             :
+        #locations       : [
+          #location      : "~ /api/integration/(.*)"
+          #proxyPass     : "http://integration/$1$is_args$args"
+        #]

     # these are unnecessary on production machines.
     # ------------------------------------------------------------------------------------------


SocialWorker: check delegate existance

SocialWorker: check delegate existance

--- workers/social/lib/social/models/datadog.coffee
+++ workers/social/lib/social/models/datadog.coffee
@@ -85,6 +85,9 @@

     { connection:{ delegate } } = client

+    unless delegate
+      return callback new KodingError 'Account not found'
+
     unless delegate?.type is 'registered'
       return callback new KodingError 'Not allowed'


payment: fixed wrong constructorname

payment: fixed wrong constructorname

--- workers/social/lib/social/models/payment.coffee
+++ workers/social/lib/social/models/payment.coffee
@@ -200,5 +200,5 @@
   fetchReferrerSpace = (client, callback)->
     originId = client.connection.delegate.getId()

-    JEarnedReward = require './rewards/earnedreward'
-    JEarnedReward.fetchEarnedAmount {originId}, callback
+    JReward = require './rewards'
+    JReward.fetchEarnedAmount {originId}, callback


payment: add confirmation requirements to backend

--- workers/social/lib/social/models/payment.coffee
+++ workers/social/lib/social/models/payment.coffee
@@ -3,6 +3,8 @@

 module.exports = class Payment extends Base
   @share()
+
+  ERR_USER_NOT_CONFIRMED = 'ERR_USER_NOT_CONFIRMED'

   @set
     sharedMethods     :
@@ -35,23 +37,27 @@
       "token", "email", "planTitle", "planInterval", "provider"
     ]

-    validateParams requiredParams, data, (err)->
+    @canUserPurchase client, (err, confirmed) ->
       return callback err  if err
+      return callback { message: ERR_USER_NOT_CONFIRMED }  unless confirmed

-      canChangePlan client, data.planTitle, (err)->
+      validateParams requiredParams, data, (err)->
         return callback err  if err

-        data.accountId = getAccountId client
-        url = "/payments/subscribe"
+        canChangePlan client, data.planTitle, (err)->
+          return callback err  if err

-        post url, data, (err, response)->
-          callback err, response
+          data.accountId = getAccountId client
+          url = "/payments/subscribe"

-          data.status = if err then "$failed" else "$success"
+          post url, data, (err, response)->
+            callback err, response

-          SiftScience = require "./siftscience"
-          SiftScience.transaction client, data, (err)->
-            log "logging to SiftScience failed", err  if err
+            data.status = if err then "$failed" else "$success"
+
+            SiftScience = require "./siftscience"
+            SiftScience.transaction client, data, (err)->
+              log "logging to SiftScience failed", err  if err

   @subscriptions$ = secure (client, data, callback)->


KDImage: change old dataURItoBlob function.

BlobBuilder and ArrayBuffer are now deprecated. This is an updated
version with Blob constructor. For more info please refer to:
http://stackoverflow.com/a/11954337/462233

--- client/Framework/components/image/KDImage.coffee
+++ client/Framework/components/image/KDImage.coffee
@@ -6,21 +6,20 @@
       if (f = args.shift()) then !!f(args) or yes else no
     , 0

+  #BlobBuilder and ArrayBuffer are now deprecated, here is an updated version
+  #with Blob constructor, #http://stackoverflow.com/a/11954337/462233
   @dataURItoBlob = (dataURI) ->
-    byteString = atob(dataURI.split(",")[1])
-    mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0]
-    ab = new ArrayBuffer byteString.length
-    ia = new Uint8Array ab
+    binary = atob(dataURI.split(",")[1])
+    array = []

     i = 0
-    while i < byteString.length
-      ia[i] = byteString.charCodeAt(i)
+    while i < binary.length
+      array.push binary.charCodeAt(i)
       i++

-    bb = new BlobBuilder
-    bb.append ab
-    bb.getBlob mimeString
-
+    new Blob([new Uint8Array(array)],
+      type: "image/png"
+    )

   constructor:(@data, @format='image/png')->
     # {@data} = file
@@ -84,4 +83,4 @@
     new KDCustomHTMLView
       tagName: 'img'
       attributes:
-        src: if 'string' is typeof @data then @data else @data.src+        src: if 'string' is typeof @data then @data else @data.src


Activity: pass item view instead of list view to openvmmodal.

--- client/Main/maincontroller/dockcontroller.coffee
+++ client/Main/maincontroller/dockcontroller.coffee
@@ -57,7 +57,7 @@

     @vmsList.getListView().on 'VMCogClicked', (vm, item)->
       {mainView} = KD.singletons
-      mainView.openVMModal vm, this
+      mainView.openVMModal vm, item

     # {mainController} = KD.singletons
     # mainController.ready @bound 'accountChanged'


Dock: finder item added

Dock: finder item added

--- client/Main/maincontroller/dockcontroller.coffee
+++ client/Main/maincontroller/dockcontroller.coffee
@@ -5,7 +5,8 @@
     { title : "Topics",    path : "/Topics",   order : 20, type :"" }
     { title : "Terminal",  path : "/Terminal", order : 30, type :"" }
     { title : "Editor",    path : "/Ace",      order : 40, type :"" }
-    { title : "Finder",    path : "/Finder",   order : 50, type :"" }
+    { title : "Apps",      path : "/Apps",     order : 50, type :"" }
+    { title : "Finder",    path : "/Finder",   order : 60, type :"" }
   ]

   createHash = (arr)->


MachineSettings: update to new route based on the frontApp when machine data modified

--- client/Main/providers/machinesettingsview.coffee
+++ client/Main/providers/machinesettingsview.coffee
@@ -88,6 +88,8 @@
       {nickEdit, nickname} = @modalTabs.forms.Settings.inputs
       label = nickEdit.getValue()

+      {appManager, router} = KD.singletons
+
       @machine.setLabel label, (err, newSlug)=>

         if not KD.showError err
@@ -97,6 +99,18 @@
           nickEdit.hide()
           nickname.show()

+          frontApp = appManager.getFrontApp()
+
+          if frontApp.options.name is "IDE" and frontApp.workspaceData?
+
+            return  unless @machine.slug is frontApp.workspaceData.machineLabel
+
+            newRoute = "/IDE/#{newSlug}/#{frontApp.workspaceData.slug}"
+            frontApp.workspaceData.machineLabel = newSlug
+
+            KD.utils.defer ->
+              computeController.once 'MachineDataUpdated', ->
+                router.clear newRoute

   viewAppended:->


Use reviewlistitemview in reviewlist

Use reviewlistitemview in reviewlist

--- client/app/CommonViews/reviews/reviewview.coffee
+++ client/app/CommonViews/reviews/reviewview.coffee
@@ -16,7 +16,7 @@

     @reviewList = new KDListView
       type          : "comments"
-      subItemClass  : CommentListItemView
+      subItemClass  : ReviewListItemView
       delegate      : @
     , data


No need those, already moved to landingpagesidebar

--- client/app/MainApp/lazy/lazydomcontroller.coffee
+++ client/app/MainApp/lazy/lazydomcontroller.coffee
@@ -11,7 +11,6 @@
     @mainController.on 'AppIsReady', =>
       if @userEnteredFromGroup()
         @addGroupViews()
-        # @switchGroupState isLoggedIn
       else if @userEnteredFromProfile()
         @addProfileViews()

@@ -20,37 +19,6 @@
   userEnteredFromGroup:-> KD.config.groupEntryPoint?

   userEnteredFromProfile:-> KD.config.profileEntryPoint?
-
-  switchGroupState:(isLoggedIn)->
-
-    {groupEntryPoint} = KD.config
-
-    loginLink = new GroupsLandingPageButton {groupEntryPoint}, {}
-
-    if isLoggedIn and groupEntryPoint?
-      KD.whoami().fetchGroupRoles groupEntryPoint, (err, roles)->
-        if err then console.warn err
-        else if roles.length
-          loginLink.setState { isMember: yes, roles }
-        else
-          {JMembershipPolicy} = KD.remote.api
-          JMembershipPolicy.byGroupSlug groupEntryPoint,
-            (err, policy)->
-              if err then console.warn err
-              else if policy?
-                loginLink.setState {
-                  isMember        : no
-                  approvalEnabled : policy.approvalEnabled
-                }
-              else
-                loginLink.setState {
-                  isMember        : no
-                  isPublic        : yes
-                }
-    else
-      @utils.defer -> loginLink.setState { isLoggedIn: no }
-
-    loginLink.appendToSelector '.group-login-buttons'

   addGroupViews:->


Tests: KodingListController: Don't pass "items" to KodingListController

--- client/app/test/kodinglistcontroller.test.coffee
+++ client/app/test/kodinglistcontroller.test.coffee
@@ -328,9 +328,7 @@

     it 'should emit "ItemsLoaded" event', ->

-      items = [ 'kodinguser', 'kodinguser2', 'kodinguser3' ]
-
-      fetcherMethod = (query, options, callback) -> callback null, items
+      fetcherMethod = (query, options, callback) -> callback null, []

       listController = new KodingListController { fetcherMethod }
       emitSpy        = expect.spyOn listController, 'emit'


Refresh decorations.

Refresh decorations.

--- lib/analysis_decorator.coffee
+++ lib/analysis_decorator.coffee
@@ -1,24 +1,42 @@
 module.exports =
 class AnalysisDecorator
+  decoratorMap: {}
   constructor: (@analysisComponent) ->
-      @analysisComponent.on 'dart-tools:analysis', (result) ->
-        for ev in atom.workspaceView.getEditorViews()
-          editor = ev.getEditor()
+    @analysisComponent.on 'dart-tools:analysis', (result) =>
+      @addDecoratorForAnalysis result
+    @analysisComponent.on 'dart-tools:refresh', (fullPath) =>
+      @refreshDecoratorsForPath fullPath

-          if editor.getPath() == result.fullpath
-            category = result.category.toLowerCase()
-            line = result.line - 1;
-            col  = result.column - 1
-            css = "dart-analysis-#{category}"
-            marker = editor.markBufferRange [
-              [line, col],
-              [line, col + result.length]
-            ]
+  addDecoratorForAnalysis: (result) ->
+    decorators = @decoratorMap[result.fullpath] ||= []
+    for ev in atom.workspaceView.getEditorViews()
+      editor = ev.getEditor()

-            editor.decorateMarker marker,
-              type: 'gutter',
-              class: css
+      if editor.getPath() == result.fullpath
+        category = result.category.toLowerCase()
+        line = result.line - 1;
+        col  = result.column - 1
+        css = "dart-analysis-#{category}"
+        marker = editor.markBufferRange [
+          [line, col],
+          [line, col + result.length]
+        ]

-            editor.decorateMarker marker,
-              type: 'highlight',
-              class: css
+        dec1 = editor.decorateMarker marker,
+          type: 'gutter',
+          class: css
+
+        dec2 = editor.decorateMarker marker,
+          type: 'highlight',
+          class: css
+
+        decorators = @decoratorMap[result.fullpath] ||= []
+        decorators.push(dec1)
+        decorators.push(dec2)
+
+        return
+
+  refreshDecoratorsForPath: (fullPath) ->
+    decorators = @decoratorMap[fullPath] || []
+    for dec in decorators
+      dec.destroy()


correctly destroy editors on show/cancel/update

--- lib/atom-ternjs-config.coffee
+++ lib/atom-ternjs-config.coffee
@@ -69,6 +69,7 @@

   clear: ->
     @hide()
+    @destroyEditors()
     @config = null
     @projectConfig = null
     @configView?.removeContent()
@@ -101,14 +102,14 @@
     @editors = []

   updateConfig: ->
+    @config.loadEagerly = []
+    @config.dontLoad = []
     for editor in @editors
       buffer = editor.getModel().getBuffer()
       text = buffer.getText()
       text = text.trim()
       continue if text is ''
-      idx = @config[editor.__ternjs_section].indexOf(text)
-      if idx is -1
-        @config[editor.__ternjs_section].push(text)
+      @config[editor.__ternjs_section].push(text)
     @destroyEditors()
     newConfig = @buildNewConfig()
     newConfigJSON = JSON.stringify(newConfig, null, 2)


resolve path correctly

resolve path correctly

--- lib/atom-ternjs-rename.coffee
+++ lib/atom-ternjs-rename.coffee
@@ -57,7 +57,7 @@

     for change in obj.changes
       change.file = change.file.replace(/^.\//, '')
-      change.file = dir.relativize(change.file)
+      change.file = path.resolve(atom.project.relativizePath(dir)[0], change.file)
     changes = _.uniq(obj.changes, (item) =>
       JSON.stringify item
     )


minor improvements

minor improvements

--- lib/autohide-tree-view.coffee
+++ lib/autohide-tree-view.coffee
@@ -1,217 +1,161 @@
 'use strict'
-{Disposable} = require 'atom'
 SubAtom = require 'sub-atom'

 class AutohideTreeView
-  {treeViewModel, treeViewResizerEl, treeViewEl} = {}
+  config: require './config'

-  config:
-    showDelay:
-      description: 'The delay  - in seconds - before the tree-view will show.'
-      type: 'integer'
-      default: 200
-      minimum: 0
-      order: 0
-    hideDelay:
-      description: 'The delay - in seconds - before the tree-view will hide.'
-      type: 'integer'
-      default: 200
-      minimum: 0
-      order: 1
-    hiddenWidth:
-      description: 'The width - in pixels - of the tree-view when minimized/hidden.'
-      type: 'integer'
-      default: 5
-      minimum: 1
-      order: 2
-    animationSpeed:
-      description: 'The speed - in 1000 pixels per second - of the sliding animation. Set to 0 to disable animation.'
-      type: 'number'
-      default: 1
-      minimum: 0
-      order: 3
-    pushEditor:
-      description: 'Push the editor to the right when showing the tree view.'
-      type: 'boolean'
-      default: false
-      order: 4
-    focusTreeViewOnOpen:
-      description: 'Focus the tree view after it opens. Not recommended because it might cause some issues with focusing other parts of the editor.'
-      type: 'boolean'
-      default: false
-      order: 5
-    unfocusTreeViewOnClose:
-      description: 'Focuses the text editor after closing the tree view. Not recommended because it might cause some issues with focusing other parts of the editor.'
-      type: 'boolean'
-      default: false
-      order: 6
-
-  activate: (state) ->
-    atom.packages.onDidActivateInitialPackages @initialize
+  activate: ->
+    @disposables = new SubAtom()
+    atom.packages.activatePackage('tree-view').then (treeViewPkg) =>
+      @treeView = treeViewPkg.mainModule.createView()
+      @treeViewResizerEl = atom.views.getView @treeView
+      @treeViewEl = @treeViewResizerEl.querySelector '.tree-view'
+      @subscribeEventHandlers()
+      @enable()
+    .catch (error) ->
+      console.error error, error.stack

   deactivate: ->
-    @initialized = false
     @disable()
-    @subs.dispose()
+    @disposables.dispose()
+    {@treeView, @treeViewResizerEl, @treeViewEl} = {}

-  initialize: =>
-    return if @initialized
-    @initialized = true
+  subscribeEventHandlers: =>
+    @disposables.add atom.config.observe 'autohide-tree-view', (@conf) =>
+      @update()
+    @disposables.add atom.config.onDidChange 'tree-view.showOnRightSide', =>
+      @update()

-    @subs = new SubAtom()
+    @disposables.add atom.commands.add 'atom-workspace',
+      'autohide-tree-view:show': =>
+        @disableHoverEvents()
+        @show true
+      'autohide-tree-view:hide': =>
+        @hide true
+      'autohide-tree-view:toggle-visible': =>
+        if @enabled
+          @disableHoverEvents()
+          @toggle()
+        else
+          atom.commands.dispatch atom.views.getView(atom.workspace), 'tree-view:toggle'
+      'autohide-tree-view:enable': =>
+        @enable()
+      'autohide-tree-view:disable': =>
+        @disable()
+      'autohide-tree-view:toggle-enabled': =>
+        if @enabled then @disable() else @enable()

-    @subs.add atom.config.observe 'autohide-tree-view', @update
-    @subs.add atom.config.observe 'tree-view.showOnRightSide', => @update()
+    @disposables.add atom.commands.add '.tree-view.autohide', 'core:cancel', =>
+      @hide true

-    registerCommand = (command, cb, namespace = 'autohide-tree-view') =>
-      @subs.add atom.commands.add 'atom-workspace', "#{namespace}:#{command}", cb
-
-    registerCommand 'show', =>
-      @disableHoverEvents()
-      @show(true)
-    registerCommand 'hide', =>
-      @hide(true)
-    registerCommand 'toggle-visible', =>
-      if @enabled
-        @disableHoverEvents()
-        @toggle()
-      else
-        atom.commands.dispatch atom.views.getView(atom.workspace), 'tree-view:toggle'
-    registerCommand 'enable', @enable
-    registerCommand 'disable', @disable
-    registerCommand 'toggle-enabled', =>
-      if @enabled then @disable() else @enable()
-    @subs.add atom.commands.add '.tree-view.autohide', 'core:cancel', =>
+    @disposables.add 'atom-workspace', 'mouseenter', '.tree-view-resizer.autohide-hover', =>
+      @show()
+    @disposables.add 'atom-workspace', 'mouseleave', '.tree-view-resizer.autohide-hover', =>
       @hide()

-    @subs.add 'atom-workspace', 'mouseenter', '.tree-view-resizer.autohide-hover', => @show()
-    @subs.add 'atom-workspace', 'mouseleave', '.tree-view-resizer.autohide-hover', => @hide()
+    @disposables.add atom.commands.add 'atom-workspace',
+      'tree-view:expand-directory': @openEntry
+      'tree-view:recursive-expand-directory': @openEntry
+      'tree-view:collapse-directory': @openEntry
+      'tree-view:recursive-collapse-directory': @openEntry
+      'tree-view:open-selected-entry': @openEntry
+      'tree-view:open-selected-entry-right': @openEntry
+      'tree-view:open-selected-entry-left': @openEntry
+      'tree-view:open-selected-entry-up': @openEntry
+      'tree-view:open-selected-entry-down': @openEntry
+      'tree-view:open-selected-entry-in-pane-1': @openEntry
+      'tree-view:open-selected-entry-in-pane-2': @openEntry
+      'tree-view:open-selected-entry-in-pane-3': @openEntry
+      'tree-view:open-selected-entry-in-pane-4': @openEntry
+      'tree-view:open-selected-entry-in-pane-5': @openEntry
+      'tree-view:open-selected-entry-in-pane-6': @openEntry
+      'tree-view:open-selected-entry-in-pane-7': @openEntry
+      'tree-view:open-selected-entry-in-pane-8': @openEntry
+      'tree-view:open-selected-entry-in-pane-9': @openEntry

-    registerCommand 'expand-directory', @openEntry, 'tree-view'
-    registerCommand 'recursive-expand-directory', @openEntry, 'tree-view'
-    registerCommand 'collapse-directory', @openEntry, 'tree-view'
-    registerCommand 'recursive-collapse-directory', @openEntry, 'tree-view'
-    registerCommand 'open-selected-entry', @openEntry, 'tree-view'
-    registerCommand 'open-selected-entry-right', @openEntry, 'tree-view'
-    registerCommand 'open-selected-entry-left', @openEntry, 'tree-view'
-    registerCommand 'open-selected-entry-up', @openEntry, 'tree-view'
-    registerCommand 'open-selected-entry-down', @openEntry, 'tree-view'
-    registerCommand 'open-selected-entry-in-pane-1', @openEntry, 'tree-view'
-    registerCommand 'open-selected-entry-in-pane-2', @openEntry, 'tree-view'
-    registerCommand 'open-selected-entry-in-pane-3', @openEntry, 'tree-view'
-    registerCommand 'open-selected-entry-in-pane-4', @openEntry, 'tree-view'
-    registerCommand 'open-selected-entry-in-pane-5', @openEntry, 'tree-view'
-    registerCommand 'open-selected-entry-in-pane-6', @openEntry, 'tree-view'
-    registerCommand 'open-selected-entry-in-pane-7', @openEntry, 'tree-view'
-    registerCommand 'open-selected-entry-in-pane-8', @openEntry, 'tree-view'
-    registerCommand 'open-selected-entry-in-pane-9', @openEntry, 'tree-view'
-    @subs.add 'atom-workspace', 'mouseup', '.tree-view-resizer .entry', (e) => process.nextTick => @openEntry e
-
-    @enable()
+    @disposables.add 'atom-workspace', 'mouseup', '.tree-view-resizer .entry', (e) =>
+      process.nextTick => @openEntry e

   enable: =>
-    return null unless @getTreeViewResizerEl()
     @enabled = true
-    treeViewResizerEl.classList.add 'autohide'
+    @treeViewResizerEl.classList.add 'autohide'
     @hide(true)
     @update()

   disable: =>
-    return unless @getTreeViewEl()
     @enabled = false
-    treeViewResizerEl.classList.remove 'autohide', 'autohide-hover'
-    treeViewResizerEl.style.width = "#{treeViewEl.clientWidth}px"
-    treeViewResizerEl.parentNode?.style?.width = ''
-    {treeViewModel, treeViewResizerEl, treeViewEl} = {}
+    @treeViewResizerEl.classList.remove 'autohide', 'autohide-hover'
+    @treeViewResizerEl.style.width = "#{@treeViewEl.clientWidth}px"
+    @treeViewResizerEl.parentNode?.style?.width = ''

-  update: (@conf = atom.config.get 'autohide-tree-view') =>
-    return unless @getTreeViewResizerEl()
+  update: =>
     if @conf.pushEditor
-      treeViewResizerEl.style.position = 'relative'
-      treeViewResizerEl.parentNode?.style?.width = ''
+      @treeViewResizerEl.style.position = 'relative'
+      @treeViewResizerEl.parentNode?.style?.width = ''
     else
-      treeViewResizerEl.style.position = 'absolute'
-      treeViewResizerEl.parentNode?.style?.width = "#{@conf.hiddenWidth}px"
+      @treeViewResizerEl.style.position = 'absolute'
+      @treeViewResizerEl.parentNode?.style?.width = "#{@conf.hiddenWidth}px"

-  show: (noDelay = false) =>
-    return unless @getTreeViewEl()
+  show: (noDelay = false) ->
+    if @conf.hiddenWidth < @treeViewResizerEl.clientWidth < @treeViewEl.clientWidth
+      noDelay = true
     setTimeout =>
-      @animate treeViewEl.clientWidth, =>
-        @visible = true
+      @visible = true
+      @animate @treeViewEl.clientWidth
     , Number(!noDelay) * @conf.showDelay
     if @conf.focusTreeViewOnOpen
-      @getTreeViewModel().focus()
+      @treeView.focus()

-  hide: (noDelay = false) =>
-    return unless @getTreeViewResizerEl()
+  hide: (noDelay = false) ->
+    if @conf.hiddenWidth < @treeViewResizerEl.clientWidth < @treeViewEl.clientWidth
+      noDelay = true
     setTimeout =>
-      @animate @conf.hiddenWidth, =>
-        @visible = false
+      @visible = false
+      @animate @conf.hiddenWidth
     , Number(!noDelay) * @conf.hideDelay
     @enableHoverEvents()
     if @conf.unfocusTreeViewOnClose
-      @getTreeViewModel().unfocus()
+      @treeView.unfocus()

   toggle: =>
-    console.log @visible
     if @visible
       @hide()
     else
       @show()

   openEntry: (e) =>
-    return unless @getTreeViewModel()
-    if treeViewModel.selectedEntry().classList.contains 'directory'
+    if @treeView.selectedEntry().classList.contains 'directory'
       @show true
     else if e.type isnt 'mouseup'
       @hide true

-  getTreeViewModel: ->
-    treeViewModel ?= do ->
-      return null unless atom.packages.isPackageActive 'tree-view'
-      pkg = atom.packages.getActivePackage 'tree-view'
-      pkg.mainModule.createView()
+  animate: do ->
+    animation = null

-  getTreeViewResizerEl: ->
-    treeViewResizerEl ?= do =>
-      @getTreeViewModel()
-      return null unless treeViewModel
-      atom.views.getView treeViewModel
+    nextFrame = ->
+      {start, duration, initialWidth, targetWidth} = animation
+      animation.rafHandle = requestAnimationFrame nextFrame.bind @
+      progress = (Date.now() - animation.start) / animation.duration
+      if 1 <= progress
+        progress = 1
+        cancelAnimationFrame animation.rafHandle
+        animation = null
+      @treeViewResizerEl.style.width = "#{initialWidth + (targetWidth - initialWidth) * progress}px"

-  getTreeViewEl: ->
-    treeViewEl ?= do =>
-      @getTreeViewResizerEl()
-      return null unless treeViewResizerEl
-      treeViewResizerEl.querySelector '.tree-view'
+    (targetWidth) ->
+      if animation?.rafHandle?
+        cancelAnimationFrame animation.rafHandle
+      initialWidth = @treeViewResizerEl.clientWidth
+      start = Date.now()
+      duration = Math.abs (targetWidth - initialWidth) / (@conf.animationSpeed or Infinity)
+      rafHandle = requestAnimationFrame nextFrame.bind @
+      animation = {targetWidth, initialWidth, start, duration, rafHandle}

-  animate: do ->
-    {rafHandle, initialWidth, widthDiff} = {}
-
-    nextFrame = (start, duration, cb) ->
-      =>
-        rafHandle = requestAnimationFrame nextFrame.call(@, start, duration, cb)
-        progress = (Date.now() - start) / duration
-        if 1 <= Math.abs progress
-          progress = progress / Math.abs progress
-          cancelAnimationFrame rafHandle
-          rafHandle = null
-          cb?()
-        @getTreeViewResizerEl().style.width = "#{initialWidth + widthDiff * progress}px"
-
-    (targetWidth, cb) ->
-      return unless @getTreeViewResizerEl()
-      if rafHandle?
-        cancelAnimationFrame rafHandle
-      initialWidth = @getTreeViewResizerEl().clientWidth
-      widthDiff = targetWidth - initialWidth
-      start = Date.now()
-      duration = Math.abs widthDiff / (@conf.animationSpeed or Infinity)
-      rafHandle = requestAnimationFrame nextFrame.call(@, start, duration, cb)

   enableHoverEvents: =>
-    @getTreeViewResizerEl()?.classList.add 'autohide-hover'
+    @treeViewResizerEl.classList.add 'autohide-hover'

   disableHoverEvents: =>
-    @getTreeViewResizerEl()?.classList.remove 'autohide-hover'
+    @treeViewResizerEl.classList.remove 'autohide-hover'

 module.exports = new AutohideTreeView()


Classroom: Prefetch for next chapter config.

--- client/app/Applications/Classroom.kdapplication/AppView.coffee
+++ client/app/Applications/Classroom.kdapplication/AppView.coffee
@@ -74,11 +74,17 @@
     return if not chapters or not @isCourseStarted()

     @readFileContent "/#{courseManifest.name}.kdcourse/#{chapters[chapterIndex].resourcesPath}", (config) =>
-      courseMeta   =
-        name       : courseManifest.name
-        index      : ++chapterIndex
+      @currentChapterIndex = chapterIndex
+      courseMeta          =
+        name              : courseManifest.name
+        index             : ++chapterIndex

       @addSubView new ClassroomWorkspace { delegate: this }, { config, courseManifest, courseMeta }
+      @fetchNextCourseConfig()
+
+  fetchNextCourseConfig: ->
+    @readFileContent "/#{@manifest.name}.kdcourse/#{@manifest.chapters[@currentChapterIndex + 1].resourcesPath}", (config) =>
+      @nextChapterConfig = config

   createCoursesView: (manifest) ->
     @addSubView new ClassroomCourseView { delegate: this }, manifest


fixes #1442: javascript literals should increase line count when they contain newlines

--- lexer.coffee
+++ lexer.coffee
@@ -181,7 +181,9 @@
   # Matches JavaScript interpolated directly into the source via backticks.
   jsToken: ->
     return 0 unless @chunk.charAt(0) is '`' and match = JSTOKEN.exec @chunk
-    @token 'JS', (script = match[0]).slice 1, -1
+    script = match[0]
+    @line += count script, '\n'
+    @token 'JS', script[1...-1]
     script.length

   # Matches regular expression literals. Lexing regular expressions is difficult


Fix misuse if Tray

Fix misuse if Tray

--- views/env.coffee
+++ views/env.coffee
@@ -95,10 +95,15 @@
         body: msg
         silent: true
     else
-      Tray.displayBalloon
+      appIcon = new Tray(path.join(ROOT, 'assets', 'icons', 'poi.ico'));
+      appIcon.displayBalloon
         title: title
         icon: icon
         content: msg
+      destroyAppIcon = ->
+        appIcon.destroy()
+      appIcon.on 'balloon-closed', destroyAppIcon
+
   if volume > 0.0001
     sound = new Audio(audio)
     sound.volume = volume


:wolf::facepunch: Updated in browser at strd6.github.io/editor

--- pixie.cson
+++ pixie.cson
@@ -1,8 +1,8 @@
-version: "0.4.0-alpha.5"
+version: "0.4.0"
 remoteDependencies: [
   "https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.5.2/underscore-min.js"
   "https://code.jquery.com/jquery-1.10.1.min.js" # TODO: Eliminate this, currently just using for Deferred
 ]
 dependencies:
-  require: "distri/require:v0.4.1"
+  require: "distri/require:v0.4.2"
   interactive: "distri/interactive:v0.8.2"


edit view

edit view

--- lib/gulp-snippets-view.coffee
+++ lib/gulp-snippets-view.coffee
@@ -4,7 +4,7 @@
 class GulpSnippetsView extends View
   @content: ->
     @div class: 'gulp-snippets overlay from-top', =>
-      @div "The GulpSnippets package is Alive! It's ALIVE!", class: "message"
+      @div "Gulp Snippets package is Active!", class: "message"

   initialize: (serializeState) ->
     atom.workspaceView.command "gulp-snippets:toggle", => @toggle()


:art: minor bug fix, precise movable check and distinct extendMovingArea function.

--- lib/move-selected-text.coffee
+++ lib/move-selected-text.coffee
@@ -24,13 +24,17 @@
   execute: ->
     @withUndoJoin =>
       selections = @editor.getSelectionsOrderedByBufferPosition()
+      topSelection = selections[0]
       selections.reverse() if @direction is 'down'
       @initOverwrittenByEditor()
       @editor.transact =>
         @countTimes =>
+          return if (not @isLinewise()) and (not @isMovable(topSelection))
+
           for selection, i in selections
             @selectionIndex = i
             @selectionIndex = selections.length - (i + 1) if @direction is 'down'
+            @extendMovingArea(selection)
             @mutate(selection) if @isMovable(selection)

   initOverwrittenByEditor: ->
@@ -119,17 +123,17 @@
     else
       @moveCharacterwise(selection)

+  extendMovingArea: (selection) ->
+    if @direction is 'down'
+      endRow = selection.getBufferRange().end.row
+      if endRow >= getVimLastBufferRow(@editor)
+        eof = @editor.getEofBufferPosition()
+        @editor.setTextInBufferRange([eof, eof], "\n")
+
   isMovable: (selection) ->
     switch @direction
-      when 'up'
-        selection.getBufferRange().start.row isnt 0
-      when 'down'
-        # Extend last buffer line if selection end is last buffer row
-        endRow = selection.getBufferRange().end.row
-        if endRow >= getVimLastBufferRow(@editor)
-          eof = @editor.getEofBufferPosition()
-          @editor.setTextInBufferRange([eof, eof], "\n")
-        true
+      when 'up' then selection.getBufferRange().start.row isnt 0
+      when 'down' then true

   # Characterwise
   # -------------------------
@@ -213,18 +217,19 @@
       when 'right' then selection.indentSelectedRows()
       when 'left' then selection.outdentSelectedRows()

+  extendMovingArea: (selection) ->
+    if not @isLinewise() and @direction is 'right'
+      {start, end} = selection.getBufferRange()
+      if pointIsAtEndOfLine(@editor, end)
+        @editor.setTextInBufferRange([end, end], " ")
+
   isMovable: (selection) ->
     if @isLinewise()
       true
     else
-      {start, end} = selection.getBufferRange()
       switch @direction
-        when 'right'
-          if pointIsAtEndOfLine(@editor, end)
-            @editor.setTextInBufferRange([end, end], " ")
-          true
-        when 'left'
-          start.column isnt 0
+        when 'right' then true
+        when 'left' then selection.getBufferRange().start.column isnt 0

   moveCharacterwise: (selection) ->
     reversed = selection.isReversed()


Rename variables for clarity

Rename variables for clarity

--- lib/notification-issue.coffee
+++ lib/notification-issue.coffee
@@ -145,10 +145,10 @@
         packagePaths[packageName] = fs.realpathSync(packagePath)

     getPackageName = (filePath) ->
-      for packageName, packagePath of packagePaths
+      for packName, packagePath of packagePaths
         continue if filePath is 'node.js'
         relativePath = path.relative(packagePath, filePath)
-        return packageName unless /^\.\./.test(relativePath)
+        return packName unless /^\.\./.test(relativePath)
       null

     if options.detail? and packageName = getPackageName(options.detail)


minor refactorings in the bower

minor refactorings in the bower

--- library/membrane/bower.coffee
+++ library/membrane/bower.coffee
@@ -79,14 +79,14 @@
     # as a bower sink and later will return it, unless overriden by
     # the global configuration. See the implementation for the info.
     @bowerSink: (sink) ->
-        assert hash = crypto.createHash "md5"
+        assert hash = try crypto.createHash "md5"
         id = hash.update(@identify()).digest "hex"
         automatic = => global or @$bowerSink or id
         global = nconf.get "bower:globalSinkDirectory"
         return automatic() if arguments.length is 0
         assert _.isString(sink), "has to be a string"
         assert not _.isEmpty(sink), "got empty sink"
-        return @$bowerSink = sink.toString()
+        return @$bowerSink = try sink.toString()

     # This is the composition hook that gets invoked when compound
     # is being composed into other services and components. Checks
@@ -114,16 +114,16 @@
     # is asynchronously wired in, so consult with `async` package.
     # Please be sure invoke the `next` arg to proceed, if relevant.
     register: (kernel, router, next) ->
-        hash = crypto.createHash "md5"
-        hash.update @constructor.identify()
-        assert id = @constructor.bowerSink()
-        bowerings = @constructor.bowerings ?= []
-        options = _.map(bowerings, (b) -> b.options)
-        options = _.merge Object.create({}), options...
+        assert hash = crypto.createHash "md5"
+        assert hash.update @constructor.identify()
+        assert id = try @constructor.bowerSink()
+        bowerings = @constructor.bowerings ?= Array()
+        options = _.map(bowerings, (bow) -> bow.options)
+        options = _.merge Object.create(Object()), options...
         directory = kernel?.scope?.envPath "pub", "bower", id
-        assert _.isString(directory), "failed to get dir"
+        assert _.isString(directory), "error with Bower dir"
         options.directory = bowerings.directory = directory
-        targets = _.map bowerings, (b) -> b.target
+        targets = _.map bowerings, (b) -> return b.target
         running = "Running Bower install for %s service"
         identify = @constructor?.identify().toString()
         logger.info running.grey, identify.underline
@@ -134,17 +134,13 @@
     # perform the installation properly. Plese refer to the register
     # hook implementation in this ABC service for more information.
     installation: (kernel, targets, options, next) ->
-        assert install = bower.commands.install
-        bowerings = @constructor.bowerings ?= []
-        installer = install targets, {}, options
-        installer.on "error", (error) -> do (error) ->
-            assert stringified = "#{error.message}#{EOL}"
-            reason = "failed Bower package installation"
-            logger.error stringified.red, error.stack
-            kernel.shutdownKernel reason.toString()
-        return installer.on "end", (installed) =>
-            assert bowerings.installed = installed
+        assert install = bower.commands.install or 0
+        bowerings = @constructor.bowerings ?= Array()
+        assert installer = install targets, {}, options
+        kernel.domain.add installer if kernel.domain.add
+        return installer.on "end", (installed) => do =>
             message = "Get Bower library %s@%s at %s"
+            assert bowerings.installed = installed or 0
             for packet in _.values(installed or Object())
                 assert meta = packet.pkgMeta or Object()
                 name = (try meta.name.underline) or null
@@ -179,13 +175,13 @@
     # This is the place where you would be importing the dependencies.
     # Pay attention that most implementations side effect the context.
     prelude: (symbol, context, request, next) ->
-        assert list = bower.commands.list
+        assert list = bower.commands.list or 0
         bowerings = @constructor.bowerings ?= []
         cached context if cached = bowerings.cached
         return next undefined if _.isFunction cached
-        options = directory: bowerings.directory
-        esc = (p) -> new RegExp RegExp.escape "#{p}"
-        match = (k) -> (b) -> esc(k).test b.target
+        assert options = directory: bowerings.directory
+        esc = (reg) -> new RegExp RegExp.escape "#{reg}"
+        match = (k) -> (b) -> return esc(k).test b.target
         sorter = (v, k) -> _.findIndex bowerings, match(k)
         finder = (v, k) -> _.find bowerings, match(k)
         list(paths: yes, options).on "end", (paths) =>


[feat] Add button to insert package from package listing

--- assets/js/panes/catalogue.coffee
+++ assets/js/panes/catalogue.coffee
@@ -71,6 +71,7 @@
   scope:
     'package': "="
     click: "&"
+    insert: "&"
   template: """

@@ -79,6 +80,11 @@

+
+          <button class="btn btn-mini" ng-click="insert(package)" tooltip="Add the selected package and its dependencies to your active plunk">
+
+          </button>
+

@@ -145,7 +151,9 @@
     $scope.markup.findAllDependencies()

   $scope.insertPackage = state.insertPackage = (pkg, verDef) ->
-    required = "#{pkg.name}@#{verDef.semver}"
+    semver = verDef?.semver or "*"
+
+    required = "#{pkg.name}@#{semver}"

     pkg.bump()

@@ -271,7 +279,7 @@
               Popular packages

-          <plunker-package-block click="openVersion(package, version)" package="package" ng-repeat="package in popular"></plunker-package-block>
+          <plunker-package-block insert="insertPackage(package)" click="openVersion(package, version)" package="package" ng-repeat="package in popular"></plunker-package-block>
           <plunker-pager nolink="true" collection="popular" nav="popular.refresh(url)"></plunker-pager>

@@ -281,7 +289,7 @@
               Search results:

-          <plunker-package-block click="openVersion(package, version)" package="package" ng-repeat="package in results"></plunker-package-block>
+          <plunker-package-block insert="insertPackage(package)" click="openVersion(package, version)" package="package" ng-repeat="package in results"></plunker-package-block>

No results found for {{query}}.

Searching for {{query}}.


disable sending metrics to pusher

disable sending metrics to pusher

--- assets/scripts/app/pusher.coffee
+++ assets/scripts/app/pusher.coffee
@@ -13,7 +13,7 @@
   init: (config) ->
     Pusher.warn = @warn.bind(this)
     Pusher.host = config.host if config.host
-    @pusher = new Pusher(config.key, encrypted: Travis.Pusher.ENCRYPTED)
+    @pusher = new Pusher(config.key, encrypted: Travis.Pusher.ENCRYPTED, disableStats: true)
     @subscribeAll(Travis.Pusher.CHANNELS) if Travis.Pusher.CHANNELS

     @callbacksToProcess = []


修正表格拖拽后不能自动刷新的BUG

修正表格拖拽后不能自动刷新的BUG

--- src/widget/list/table.coffee
+++ src/widget/list/table.coffee
@@ -508,6 +508,7 @@
 								ui.helper.dragOverColumn = null
 								$fly(@_getColumnInsertIndicator(tableHeader)).addClass("hidden")
 						drop: (evt, ui)=>
+							shouldRefreshTable = false
 							draggingColumn = ui.helper.draggingColumn
 							dragOverColumn = ui.helper.dragOverColumn
 							columns = dragOverColumn._parent?._columns
@@ -526,12 +527,16 @@
 								else
 									columns.splice(i, 0, draggingColumn)
 								dragOverColumn._parent.set("columns", columns)
+								shouldRefreshTable = true

 							if ui.helper.dragOverCell
 								tableHeader = $fly(ui.helper.dragOverCell).closest(".table-header")[0]
 								ui.helper.dragOverCell = null
 								ui.helper.dragOverColumn = null
 								$fly(@_getColumnInsertIndicator(tableHeader)).addClass("hidden")
+
+							if shouldRefreshTable
+								@_onColumnChange(true)
 							return
 					)
 				return


Implemented some branch instructions.

Implemented some branch instructions.

--- src/CPU.coffee
+++ src/CPU.coffee
@@ -14,8 +14,16 @@
     IndirectIndexedY: 13

 Instruction =
-    ADC: 1
-    AND: 2
+    ADC:  1
+    AND:  2
+    ASL:  3
+    BCC:  4
+    BCS:  5
+    BEQ:  6
+    BIT:  7
+    BMI:  8
+    BNE:  9
+    BPL: 10

 Interrupt =
     IRQ:   1
@@ -86,11 +94,11 @@
         @instructionsTable = []

         @registerInstruction Instruction.ADC, (address) ->
-            addition = @read address
-            result = @accumulator + addition + @carryFlag
+            operand = @read address
+            result = @accumulator + operand + @carryFlag
             @computeCarryFlag result
             @computeZeroFlag result
-            @computeOverflowFlag @accumulator @addition @result
+            @computeOverflowFlag @accumulator, operand, @result
             @computeNegativeFlag result
             @accumulator = result & 0xFF

@@ -99,6 +107,45 @@
             @computeZeroFlag @accumulator
             @computeNegativeFlag @accumulator

+        @registerInstruction Instruction.ASL, (address) ->
+            if address?
+                result = (@read address) << 1
+                @computeCarryFlag result
+                @computeZeroFlag result
+                @computeNegativeFlag result
+                @write address, result & 0xFF
+            else
+                result = @accumulator << 1
+                @computeCarryFlag result
+                @computeZeroFlag result
+                @computeNegativeFlag result
+                @accumulator = result & 0xFF
+
+        @registerInstruction Instruction.BCC, (address) ->
+            @branchIfTrue, @carryFlag == 0, address
+
+        @registerInstruction Instruction.BCS, (address) ->
+            @branchIfTrue, @carryFlag == 1, address
+
+        @registerInstruction Instruction.BEQ, (address) ->
+            @branchIfTrue @zeroFlag == 1,  address
+
+        @registerInstruction Instruction.BIT, (address) ->
+            operand = @read address
+            result = @accumulator & operand
+            @computeZeroFlag result
+            @overflowFlag = (result >> 7) & 1 # Exception on overflow computation
+            @computeNegativeFlag result
+
+        @registerInstruction Instruction.BMI, (address) ->
+            @branchIfTrue @negativeFlag == 1, address
+
+        @registerInstruction Instruction.BNE, (address) ->
+            @branchIfTrue @zeroFlag == 0,  address
+
+        @registerInstruction Instruction.BPL, (address) ->
+            @branchIfTrue @negativeFlag == 0,  address
+
     computeCarryFlag: (result) ->
         @carryFlag = if result > 0xFF then 1 else 0

@@ -110,6 +157,11 @@

     computeNegativeFlag: (result) ->
         @negativeFlag = (result >> 7) & 1
+
+    branchIfTrue: -> (condition, address)
+        if condition then
+            @programCounter = address
+            @branchTaken = true

     registerInstruction: (instruction, execution) ->
         @instructionsTable[instruction] = execution
@@ -135,6 +187,26 @@
         @registerOperation 0x21, Instruction.AND, AddressingMode.IndexedXIndirect, 2, 6
         @registerOperation 0x31, Instruction.AND, AddressingMode.IndirectIndexedY, 2, 5

+        @registerOperation 0x0A, Instruction.ASL, AddressingMode.Accumulator,      1, 2
+        @registerOperation 0x06, Instruction.ASL, AddressingMode.ZeroPage,         2, 5
+        @registerOperation 0x16, Instruction.ASL, AddressingMode.IndexedXZeroPage, 2, 6
+        @registerOperation 0x0E, Instruction.ASL, AddressingMode.Absolute,         3, 6
+        @registerOperation 0x1E, Instruction.ASL, AddressingMode.IndexedXAbsolute, 3, 7
+
+        @registerOperation 0x90, Instruction.BCC, AddressingMode.Relative,         2, 2
+
+        @registerOperation 0xB0, Instruction.BCS, AddressingMode.Relative,         2, 2
+
+        @registerOperation 0xB0, Instruction.BEQ, AddressingMode.Relative,         2, 2
+
+        @registerOperation 0x24, Instruction.BIT, AddressingMode.ZeroPage,         2, 3
+        @registerOperation 0x2C, Instruction.BIT, AddressingMode.Absolute,         3, 4
+
+        @registerOperation 0x30, Instruction.BMI, AddressingMode.Relative,         2, 2
+
+        @registerOperation 0xD0, Instruction.BNE, AddressingMode.Relative,         2, 2
+
+        @registerOperation 0x10, Instruction.BPL, AddressingMode.Relative,         2, 2

     registerOperation: (operationCode, instruction, addressingMode, size, cycles) ->
         @operationsTable[operationCode] =


Deal with projects with no Atom Tracker config

--- lib/atom-tracker.coffee
+++ lib/atom-tracker.coffee
@@ -7,6 +7,7 @@
 StatusBarView = require './views/status-bar-view'

 NO_PROJECT = 'Atom Tracker requires an active Atom project'
+NO_TRACKER = 'Atom Tracker is not configured for this project'

 module.exports = AtomTracker =

@@ -27,6 +28,9 @@
       'atom-tracker:next-story': => @selectNextStory()
     @subscriptions.add atom.commands.add 'atom-workspace',
       'atom-tracker:finish-story': => @finishCurrentStory()
+      'atom-tracker:next-story': => @selectNextStory()
+    @subscriptions.add atom.commands.add 'atom-workspace',
+      'atom-tracker:create-story': => @createNewStory()
     # Monitor configuration changes
     atom.config.onDidChange 'atom-tracker.showStatusBar',
       ({newValue, oldValue}) =>
@@ -54,19 +58,31 @@

   selectNextStory: ->
     if atom.project.getPaths().length > 0
-      new SelectStoryStartListView(
-        @projectData.project,
-        @state.atomTrackerViewState
-      ).reveal()
+      if @projectData
+        new SelectStoryStartListView(
+          @projectData.project,
+          @state.atomTrackerViewState
+        ).reveal()
+      else
+        atom.notifications.addWarning NO_TRACKER, {icon: 'graph'}
+    else
+      atom.notifications.addWarning NO_PROJECT, {icon: 'graph'}
+
+  createNewStory: ->
+    if atom.project.getPaths().length > 0
+      new CreateStoryView(@state.atomTrackerViewState)
     else
       atom.notifications.addWarning NO_PROJECT, {icon: 'graph'}

   finishCurrentStory: ->
     if atom.project.getPaths().length > 0
-      new SelectStoryFinishListView(
-        @projectData.project,
-        @state.atomTrackerViewState
-      ).reveal()
+      if @projectData
+        new SelectStoryFinishListView(
+          @projectData.project,
+          @state.atomTrackerViewState
+        ).reveal()
+      else
+        atom.notifications.addWarning NO_TRACKER, {icon: 'graph'}
     else
       atom.notifications.addWarning NO_PROJECT, {icon: 'graph'}

@@ -78,13 +94,14 @@
       atom.notifications.addWarning NO_PROJECT, {icon: 'graph'}

   readProjectConfig: ->
-    paths = atom.project.getPaths()
-    if paths.length > 0
+    if atom.project.getPaths().length > 0
       FileUtils.readCsonFile FileUtils.rootFilepath(), (results) =>
         @projectData = results
         tile = @statusBarTile?.getItem()
         tile.updateContent results
         tile.display atom.config.get 'atom-tracker.showStatusBar'
+    else
+      atom.notifications.addWarning NO_PROJECT, {icon: 'graph'}

   deactivate: ->
     @subscriptions.dispose()


Fix error thrown when formatting empty C++ files

Fixes LiquidHelium/atom-clang-format#67.

--- lib/clang-format.coffee
+++ lib/clang-format.coffee
@@ -113,6 +113,7 @@
     return -1

   getReturnedCursorPosition: (stdout) ->
+    return 0 unless stdout
     parsed = JSON.parse stdout.slice(0, @getEndJSONPosition(stdout))
     return parsed.Cursor


Support HTTPS; Fix bugs w/ error handling

--- lib/connect-emma.coffee
+++ lib/connect-emma.coffee
@@ -1,7 +1,8 @@
-http = require 'http'
-url  = require 'url'
-path = require 'path'
-gm   = require 'gm'
+http  = require 'http'
+https = require 'https'
+url   = require 'url'
+path  = require 'path'
+gm    = require 'gm'

 class Route
   constructor: (pathname) ->
@@ -26,7 +27,8 @@
       this.failWithError(err)

   doFetch: (urlString) ->
-    http.get(url.parse(urlString), (imageResponse) =>
+    protocol = if (0 == urlString.indexOf('https')) then https else http
+    protocol.get(url.parse(urlString), (imageResponse) =>
       try
         if imageResponse.statusCode == 200
           this.processImage(imageResponse, urlString)
@@ -41,11 +43,11 @@
       console.log("[ERR] error fetching image: #{err.message}")
       this.failWithError(err)

-    ).on('close', (err) =>
-      console.log("[ERR] error fetching image: #{err.message}")
-      this.failWithError(err)
+    ).on('close', =>
+      console.log("[ERR] connection closed")
+      this.fail(500, 'Connection closed')

-    ).on('socket', (socket) ->
+    ).on('socket', (socket) =>
       socket.setTimeout(@ns.timeout) if @ns.timeout?
     )

@@ -87,7 +89,7 @@
       'Content-Type': 'text/plain'
       'Cache-Control': 'no-cache'
     )
-    @response.end(err.message)
+    @response.end(message)

 module.exports = (options) ->
   (req, res, next) ->
@@ -105,4 +107,4 @@

     else
       next()
-  +