Update ukrainian l18n

Update ukrainian l18n

Translated more lines, fix wrong translations, fix language specific things

--- app/locale/uk.coffee
+++ app/locale/uk.coffee
@@ -80,7 +80,7 @@
     adjust_volume: "Підлаштувати гучність"
     campaign_multiplayer: "Арени для мультиплеєра"
     campaign_multiplayer_description: "... в яких Ви програмуєте віч-на-віч із іншими гравцями."
-    campaign_old_multiplayer: "(Застарілий) Стара Мультіплеер Арена"
+    campaign_old_multiplayer: "(Застаріло) Стара багатокористувацька арена"
 #    campaign_old_multiplayer_description: "Relics of a more civilized age. No simulations are run for these older, hero-less multiplayer arenas."

   share_progress_modal:
@@ -94,24 +94,24 @@
   login:
     sign_up: "створення акаунту"
     log_in: "Увійти"
-    logging_in: "Вхід в акаунт"
+    logging_in: "Триває процес входу"
     log_out: "Вийти"
-    forgot_password: "Забули свій пароль?"
+    forgot_password: "Дірява пам'ять?"
     authenticate_gplus: "Авторизація G+"
     load_profile: "Завантажити профіль G+"
     finishing: "Завершення"
     sign_in_with_facebook: "Увійти через Facebook"
     sign_in_with_gplus: "Увійти через G+"
-    signup_switch: "Хочете створити акаунт?"
+    signup_switch: "Хочете створити екаунт?"

   signup:
     email_announcements: "Отримувати анонси на email"
-    creating: "Створення акаунта..."
+    creating: "Створення екаунту..."
     sign_up: "Реєстрація"
     log_in: "вхід з паролем"
     social_signup: "Або Ви можете створити акаунт через Facebook чи G+:"
-    required: "Вам потрібно увійти, щоби виконати цю дію."
-    login_switch: "Уже маєте акаунт?"
+    required: "Ця дія потребує входу."
+    login_switch: "Уже маєте екаунт?"

   recover:
     recover_account_title: "Відновити акаунт"
@@ -200,12 +200,12 @@
     wizard: "Чаклун"

   units:
-    second: "Секунда"
-    seconds: "Секунд"
-    minute: "Хвилина"
-    minutes: "Хвилин"
-    hour: "Година"
-    hours: "Годин"
+    second: "секунда"
+    seconds: "секунд"
+    minute: "хвилина"
+    minutes: "хвилин"
+    hour: "година"
+    hours: "годин"
     day: "день"
     days: "днів"
     week: "тиждень"
@@ -254,12 +254,12 @@
     victory_review_placeholder: "Вам сподобався рівень?"
     victory_hour_of_code_done: "Ви закінчили?"
     victory_hour_of_code_done_yes: "Так, я закінчив Годину Коду!"
-    victory_experience_gained: "Отриманий досвід"
-    victory_gems_gained: "Отримані самоцвіти"
+    victory_experience_gained: "Отримано досвіду"
+    victory_gems_gained: "Отримано самоцвітів"
     victory_new_item: "Новий предмет"
-    victory_viking_code_school: "Оце так! Це був важкий рівень, але Ви справились! Якщо Ви ще не розробник програмного забезпечення, Вам слід вже бути. Ви щойно пройшли швидку процедуру прийняття у Viking Code School, де Ви матимете змогу удосконалити Ваші навички до наступного рівня та стати професійним веб-розробником за 14 тижнів."
+    victory_viking_code_school: "Хай йому грець! Цей рівень був важким, але Ви справились! Якщо Ви ще не програміст, вам слід ним стати. Ви щойно екстерном пройшли процедуру прийняття у Viking Code School, де Ви матимете змогу значно покращити свої здібності та стати професійним веб-розробником за 14 тижнів."
     victory_become_a_viking: "Стати вікінгом"
-    victory_bloc: "Відмінна робота! Ваші навички покращуються! Якщо ви роздумуєте, як стати розробником програмного забезпечення, це може бути ваш щасливий день. Bloc являє собою онлайн навчальний табір, який дає вам досвідченого наставника, який допоможе Вам стати професійним розробником! Перемігши Mayhem of Munchkins, ви маєте право на $ 500 зменшення ціни з кодом: CCRULES"
+    victory_bloc: "Відмінна робота! Ваші навички покращуються! Якщо ви роздумуєте, як стати розробником програмного забезпечення, це може бути ваш щасливий день. Bloc являє собою навчальний табір онлайн, який дає вам досвідченого наставника, який допоможе Вам стати професійним розробником! Перемігши Mayhem of Munchkins, ви маєте право на знижку у 500$ з кодом: CCRULES"
     victory_bloc_cta: "Зустріньте свого наставника - дізнайтеся більше про Bloc"
     guide_title: "Посібник"
     tome_minion_spells: "Заклинання Ваших міньонів" # Only in old-style levels.
@@ -301,10 +301,10 @@
     tip_tell_friends: "Сподобався CodeCombat? Розкажіть своїм друзям про нас!"
     tip_beta_launch: "Бета-версія CodeCombat вийшла в жовтні 2013."
     tip_think_solution: "Думай над рішенням, а не над проблемою."
-    tip_theory_practice: "У теорії між теорією і практикою немає ніякої різниці. На практиці - є. - Йоґі Берра"
+    tip_theory_practice: "У теорії між теорією і практикою немає ніякої різниці. На практиці - є.  Йоґі Берра"
     tip_error_free: "Є два шляхи написання програм без помилок; але працює лише третій. - Алан Перліс"
     tip_debugging_program: "Якщо налагодження є процесом усунення помилок, то програмування повинно бути процесом їх створення. - Едсґер В. Дейкстра"
-    tip_forums: "Заходьта на форум і розкажіть нам, що Ви думаєте!"
+    tip_forums: "Заходьте на форум і розкажіть нам, що Ви думаєте!"
     tip_baby_coders: "У майбутньому, навіть малюки будуть Архімагами."
     tip_morale_improves: "Завантаження буде продовжуватися, поки мораль не покращиться."
     tip_all_species: "Ми віримо у рівні можливості вивчення програмування для всіх."
@@ -326,9 +326,9 @@
     tip_brute_force: "Якщо Ви сумніваєтеся, використовуйте перебір. - Кен Томсон"
     tip_extrapolation: "Існує 2 типи людей: які можуть екстраполюватись від неповних даних..."
     tip_superpower: "Написання коду - це найближче до суперсили, що у нас є."
-    tip_control_destiny: "Зі справді відкритим кодом у тебе є право контролювати свою власну долю. - Лінус Торвальдс"
+    tip_control_destiny: "Зі справді відкритим кодом у тебе є право контролювати свою власну долю. – Лінус Торвальдс"
     tip_no_code: "Немає коду швидшого, ніж відсутній код."
-    tip_code_never_lies: "Код ніколи не бреше. Коментарі - іноді. — Ron Jeffries"
+    tip_code_never_lies: "Код ніколи не бреше. Коментарі - іноді. — Рон Джефріс"
     tip_reusable_software: "Щоб програмне забезпечення можна було використовувати повторно, треба, щоб його можна було використовувати."
     tip_optimization_operator: "У кожній мові є свій оператор оптимізації. У більшості мов це ‘//’"
     tip_lines_of_code: "Вимірювати прогрес програмування кількістю рядків коду - це як вимірювати прогрес літакобудування вагою. — Білл Ґейтс"
@@ -364,7 +364,7 @@

   leaderboard:
     leaderboard: "Таблиця лідерів"
-    view_other_solutions: "Подивитись розв'язки інших" # {change}
+    view_other_solutions: "Переглянути таблицю лідерів"
     scores: "Рейтинг"
     top_players: "Сортувати гравців за"
     day: "Сьогодні"
@@ -394,7 +394,7 @@
     pile_gems: "Купа самоцвітів"
     chest_gems: "Скриня самоцвітів"
     purchasing: "Купування.."
-    declined: "Вашу карту відхилено"
+    declined: "Вашу картку відхилено"
     retrying: "Помилка сервера, повторна спроба."
     prompt_title: "Недостатньо самоцвітів"
     prompt_body: "Хочете отримати ще?"
@@ -424,34 +424,34 @@
     sorry_to_see_you_go: "Шкода, що Ви йдете! Будь ласка, дайте нам знати, що ми могли б зробити краще."
     unsubscribe_feedback_placeholder: "О, що ж ми зробили?"
     parent_button: "Спитати у батьків"
-    parent_email_description: "Ми відправимо ім електронний лист, щоб вони могли придбати тобі підписку."
-    parent_email_input_invalid: "Введено невірну електронну адресу."
+    parent_email_description: "Ми відправимо їм електронний лист, щоб придбати тобі підписку."
+    parent_email_input_invalid: "Уведено невірну електронну адресу."
     parent_email_input_label: "Електронна адреса батьків"
-    parent_email_input_placeholder: "Введи електронну адресу батьків"
+    parent_email_input_placeholder: "Уведи електронну адресу батьків"
     parent_email_send: "Відправити лист"
     parent_email_sent: "Лист відправлено!"
     parent_email_title: "Яка в твоїх батьків електронна адреса?"
     parents: "Батькам"
-    parents_title: "Дорога мамо/батьку, ваша дитина вчиться програмувати. Чи допоможите ви їй продовжити цю спрову?" # {change}
-    parents_blurb1: "Разом з CodeCombat Ваша дитина писатиме реальний код. Почне з простих команд та поступово буде розвиватись до складніших тем." # {change}
+    parents_title: "Дорога мамо/батьку, ваша дитина вчиться програмувати. Чи допоможите ви їй продовжити цю спрову?"
+    parents_blurb1: "Ваша дитина уже завершила __nLevels__ рівнів та вивчила базові речі. Допоможіть культивувати її інтерес та придбайте їй абонемент аби вона могла продовжити грати."
     parents_blurb1a: "Коп'ютерне програмування є необхідними вмінням, що ваша дитина беззаперечно використовуватиме у дорослому віці. До 2020 року 77% професій потребуватимуть базових навичок у програмному забезпечені, а програмісти надзвичайно потрібні у всьому світі. Чи знали ви, що Комп'ютерні Науки - це найбільш високооплачувана університетьська спеціальність?"
     parents_blurb2: "За 9.99$ на місяць, вона отримуватиме нові завдання щотижня та персональні листи підтримки від професійних програмістів." # {change}
     parents_blurb3: "Жодного ризику: 100% гарантія повернення грошей, легке скасування абонементу одним кліком."
     payment_methods: "Платіжні методи"
     payment_methods_title: "Платіжні методи, що приймаються"
-    payment_methods_blurb1: "Наразі ми приймаємо кредитні картник та Alpiay."
+    payment_methods_blurb1: "Наразі ми приймаємо кредитні картки та Alpiay."
     payment_methods_blurb2: "Якщо Вам необхідно використати інший спосіб оплати, будь ласка, зв'яжіться з нами."
     sale_already_subscribed: "Ви вже підписалися!"
-    sale_blurb1: "Знижки 35%"
-    sale_blurb2: "від звичайної ціни підписки $ 120 протягом цілого року!"
-    sale_button: "Знижки!"
-    sale_button_title: "Знижки 35% при покупці підписки на 1 рік"
+    sale_blurb1: "Зекономте 35%"
+    sale_blurb2: "від звичайної ціни у 120$ на рік!"
+    sale_button: "Розпродаж!"
+    sale_button_title: "Збережіть 35% при покупці абонементу на 1 рік"
     sale_click_here: "Натисніть тут"
     sale_ends: "Kінець"
     sale_extended: "*Існуючі підписки буде продовжено на 1 рік."
     sale_feature_here: "Ось те, що ви отримаєте:"
-    sale_feature2: "Доступ до 9 потужних нових героїв з унікальними навичками!"
-    sale_feature4: " 42000 бонусних каменівидані негайно!"
+    sale_feature2: "Доступ до 9 потужних нових героїв з унікальними вміннями!"
+    sale_feature4: "Миттєве отримання  42000 бонусних самоцвітів!"
     sale_continue: "Готовий продовжити пригоди?"
     sale_limited_time: "Обмежена пропозиція!"
     sale_new_heroes: "Нові герої!"
@@ -493,8 +493,8 @@
     choose_hero: "Оберіть героя"
     programming_language: "Мова програмування"
     programming_language_description: "Яку мову програмування Ви хочете використовувати?"
-    default: "Типова"
-    experimental: "Експериментальна"
+    default: "Типово"
+    experimental: "Експериментально"
     python_blurb: "Проста, але потужна, чудово підходить як новачкам, так і експертам."
     javascript_blurb: "Мова веб-сторінок. (Не плутати з Java.)"
     coffeescript_blurb: "Покращений синтаксис JavaScript."
@@ -528,7 +528,7 @@
     restricted_to_certain_heroes: "Тільки певні герої можуть грати в цьому рівні."

   skill_docs:
-    writable: "записний" # Hover over "attack" in Your Skills while playing a level to see most of this
+    writable: "дозволено запис" # Hover over "attack" in Your Skills while playing a level to see most of this
     read_only: "тільки читання"
     action: "Дія"
     spell: "Закляття"
@@ -607,9 +607,9 @@
     intro_1: "CodeCombat - це онлайн гра, що вчить програмуванню. Студенти пишуть код на реальних мовах програмування."
     intro_2: "Досвід не потрібен!"
     free_title: "Скільки це коштує?"
-    cost_premium_server: "CodeCombat безкоштовний протягом перших п'яти рівнів, після чого він коштує $ 9,99 USD в місяць за доступ до інших наших 190+ рівням на наших ексклюзивних серверах."
+    cost_premium_server: "CodeCombat безкоштовний протягом перших п'яти рівнів, після чого він коштуватиму 9,99$ на місяць за доступ до інших наших 190+ рівнів на наших ексклюзивних серверах."
     free_1: "Є 110+ БЕЗКОШТОВНИХ рівнів, які охоплюють всі поняття."
-    free_2: "Місячна підписка надає доступ до відео-уроків і додаткових рівнів практики."
+    free_2: "Місячний абонемент надає доступ до відео-уроків і додаткових рівнів практики."
     teacher_subs_title: "Вчителі отримують безкоштовні підписки!"
     teacher_subs_0: "Ми пропонуємо безкоштовну передплату для тестування студентів."
     teacher_subs_1: "Будь ласка, заповніть "
@@ -618,13 +618,13 @@
     sub_includes_title: "Що входить у підписку?"
     sub_includes_1: "На додаток до 110+ основних рівнів, студенти з щомісячною підпискою отримають доступ до цих додаткових функцій:"
     sub_includes_2: "80+ рівнів практики"
-    sub_includes_3: "Відео уроки"
-    sub_includes_4: "Преміум підтримка по електронній пошті"
-    sub_includes_5: "10 нових героїв з унікальними навичками для оволодіння"
+    sub_includes_3: "Відео-уроки"
+    sub_includes_4: "Преміум-підтримка електронною поштою"
+    sub_includes_5: "10 нових героїв з унікальними вміннями якими можна оволодіти"
     sub_includes_6: "3500 бонусних самоцвітів кожен місяць"
     sub_includes_7: "Приватні клани"
     monitor_progress_title: "Як мені стежити за прогресом студентів?"
-    monitor_progress_1: "Прогрес студентів може бути відстежити, створивши"
+    monitor_progress_1: "Прогрес студентів можна відстежувати, створивши"
     monitor_progress_2: "для вашого класу."
     monitor_progress_3: "Щоб додати студентів, надішліть їм посилання-запрошення в ваш Клан, яке можна знайти на"
     monitor_progress_4: "сторінці."
@@ -633,18 +633,18 @@
     private_clans_2: "Щоб створити приватний Клан, відзначте прапорець 'Зробити клан приватним', коли будете створювати"
     private_clans_3: "."
     who_for_title: "Для кого призначений CodeCombat?"
-    who_for_1: "Ми рекомендуємо CodeCombat для учнів старше 9 років. Hіякий досвід програмування не потрібно."
+    who_for_1: "Ми рекомендуємо CodeCombat для учнів старше 9 років. Попередній досвід програмування не потрібен."
     who_for_2: "Ми розробили CodeCombat так, щоб він підходив і хлопчикам і дівчаткам."
     material_title: "Як багато тут матеріалу?"
     material_premium_server: "Близько 50 годин ігрового процесу, розподіленого більш ніж на 190 рівнів для передплатників."
     material_1: "Приблизно 25 годин безкоштовного контенту і додаткові 15 годин контенту для передплатників."
     concepts_title: "Про які концепти ми розповідаємо?"
-    how_much_title: "Скільки коштує місячна передплата?"
+    how_much_title: "Скільки коштує місячний абонемент?"
     how_much_1: ""
-    how_much_2: "Місячна передплата"
-    how_much_3: "коштує $9.99, та може бути скасована будь-коли."
+    how_much_2: "Місячний абонемент"
+    how_much_3: "коштує 9.99$, та може бути скасованим будь-коли."
     how_much_4: "Крім цього, ми надаємо знижки для великих груп:"
-    how_much_5: "Ми надаємо знижку на разові закупівлі та річну передплату для груп, таких як клас або школа. Будь ласка, зв'яжіться з нами"
+    how_much_5: "Ми надаємо знижку на разові закупівлі та річну передплату для груп, як-от клас або школа. Будь ласка, зв'яжіться з нами"
     how_much_6: "для отримання більш детальної інформації."
     more_info_title: "Де я можу знайти більше інформації?"
     more_info_1: "Наш"
@@ -656,36 +656,36 @@

   teachers_survey:
     title: "Анкета вчителя"
-    must_be_logged: "Спочатку необхідно увійти в акаунт. Будь ласка, створіть акаунт або увійдіть через меню вгорі."
+    must_be_logged: "Спочатку необхідно увійти до екаунту. Будь ласка, створіть екаунт або увійдіть через меню вгорі."
     retrieving: "Отримання інформації..."
     being_reviewed_1: "Ваша заявка на безкоштовну пробну підписку зараз"
-    being_reviewed_2: "на розгляді."
-    approved_1: "Ваша заявка на безкоштовну пробну підписку була"
-    approved_2: "затверджена."
+    being_reviewed_2: "розглядається."
+    approved_1: "Вашу заявку на безкоштовну пробну підписку було"
+    approved_2: "схвалено."
     approved_3: "Подальші інструкції були вислані на"
-    denied_1: "Ваша заявка на безкоштовну пробну підписку була"
-    denied_2: "відхилена."
+    denied_1: "Вашу заявку на безкоштовну пробну підписку було"
+    denied_2: "відхилено."
     contact_1: "Будь ласка, зв'яжіться з"
     contact_2: ", якщо у вас залишилися питання."
     description_1: "Ми пропонуємо безкоштовні підписки вчителям в цілях ознайомлення. Ви можете знайти більше інформації на нашій сторінці"
     description_2: "вчителів"
     description_3: "."
-    description_4: "Будь ласка, заповніть цю маленьку анкету і ми надішлемо вам інструкції з установки на email."
+    description_4: "Будь ласка, заповніть цю маленьку анкету і ми надішлемо вам інструкції з настановами на email."
     email: "Email-адреса"
-    school: "Найменування школи"
-    location: "Найменування міста"
-    age_students: "Скільки років вашим студентам?"
+    school: "Назва школи"
+    location: "Назва міста"
+    age_students: "Скільки років вашим учням?"
     under: "Менше"
     other: "Інше:"
-    amount_students: "Як багато студентів ви навчаєте?"
-    hear_about: "Як ви дізналися про CodeCombat?"
+    amount_students: "Як багато учнів ви навчаєте?"
+    hear_about: "Звідки ви дізналися про CodeCombat?"
     fill_fields: "Будь ласка, заповніть всі поля."
-    thanks: "Дякую! Скоро ми надішлемо вам інструкцію по встановленню."
+    thanks: "Дякуємо! Незабаром ми надішлемо вам інструкцію з настановами."

   versions:
     save_version_title: "Зберегти нову версію"
     new_major_version: "Зберегти основну версію"
-    submitting_patch: "Відправка патчу..."
+    submitting_patch: "Триває надсилання патчу..."
     cla_prefix: "Для збереження змін спочатку треба погодитись з нашим"
     cla_url: "CLA"
     cla_suffix: "."
@@ -698,12 +698,12 @@
     forum_prefix: "Для будь-яких публічних обговорень, будь ласка, використовуйте "
     forum_page: "наш форум"
     forum_suffix: "."
-    faq_prefix: "Також у вас є"
+    faq_prefix: "Також у нас є"
     faq: "ЧаПи"
     subscribe_prefix: "Якщо вам потрібна допомога у проходженні рівня, будь ласка"
     subscribe: "купіть підписку CodeCombat"
     subscribe_suffix: "і ми будемо раді допомогти вам з вашим кодом."
-    subscriber_support: "Оскільки ви підписчик CodeCombat, ваші електронні листи отримають нашу пріорітетну підтримку."
+    subscriber_support: "Оскільки ви власник абонементу CodeCombat, ваші листи розглядаються у пріоритеті."
     screenshot_included: "Приєднано зняток."
     where_reply: "Куди ми повинні відповісти?"
     send: "Надіслати відгук"
@@ -801,7 +801,7 @@
     new_name: "Назва нового клану"
     new_description: "Опис нового клану"
     make_private: "Зробити клан приватним"
-    subs_only: "лише для підписчиків"
+    subs_only: "лише для підписників"
     create_clan: "Створити новий клан"
     private_preview: "Прев'ю"
     public_clans: "Публічні клани"
@@ -812,7 +812,7 @@
     type: "Тип"
     edit_clan_name: "Змінити назву клану"
     edit_clan_description: "Змінити опис клану"
-    edit_name: "Змінити ім'я"
+    edit_name: "Змінити назву"
     edit_description: "Змінити опис"
     private: "(закритий)"
     summary: "Загалом"
@@ -839,19 +839,19 @@
     playtime: "Тривалість гри"
     last_played: "Остання гра"
     leagues_explanation: "Грайте в лізі проти інших членів клану на мультіплєєрній арені."
-#    track_concepts1: "Track concepts"
+    track_concepts1: "Слідкувати за поняттями"
     track_concepts2a: "вивчено усіма студентами"
     track_concepts2b: "вивчено усіма учасниками"
-#    track_concepts3a: "Track levels completed for each student"
-#    track_concepts3b: "Track levels completed for each member"
-#    track_concepts4a: "See your students'"
-#    track_concepts4b: "See your members'"
+    track_concepts3a: "Слідкувати за рівнями, що завершили усі учні"
+    track_concepts3b: "Слідкувати за рівнями, що завершили усі учасники"
+    track_concepts4a: "Переглянути учнів"
+    track_concepts4b: "Переглянути учасників"
     track_concepts5: "рішення"
-#    track_concepts6a: "Sort students by name or progress"
-#    track_concepts6b: "Sort members by name or progress"
-    track_concepts7: "Потребує запрошення"
-#    track_concepts8: "to join"
-#    private_require_sub: "Private clans require a subscription to create or join."
+    track_concepts6a: "Впорядкувати учнів за ім'ям або прогресом"
+    track_concepts6b: "Впорядкувати учасників за ім'ям або прогресом"
+    track_concepts7: "Необхідне запрошення"
+    track_concepts8: "аби приєднатись"
+    private_require_sub: "Створення або приєданння до приватного клану потребує абонемент."

   courses:
     course: "Курс"
@@ -887,20 +887,20 @@
     invite_link_header: "Посилання для запрошення в клас"
     invite_link_p_1: "Дайте це посилання студентам, яких ви б хотіли запросити в клас."
     invite_link_p_2: "Або ми самі можемо написати їм листа на пошту:"
-#    capacity_used: "Course slots used:"
-    enter_emails: "Введіть email студентів по одному на рядку, яких ви хочете запросити"
+    capacity_used: "Використано слотів курсу:"
+    enter_emails: "Введіть email-и студентів по одному на рядок, яких ви хочете запросити"
     send_invites: "Відправити запрошення"
     title: "Тема"
     description: "Опис"
     languages_available: "Виберіть мови програмування, доступні класу:"
     all_lang: "Всі мови"
     show_progress: "Показувати прогрес студентів всьому класу"
-    creating_class: "Створення класу ... "
-    purchasing_course: "Покупка курсу ... "
+    creating_class: "Триває створення класу ... "
+    purchasing_course: "Триває придбання курсу ... "
     buy_course: "Купити курс"
     buy_course1: "Купити цей курс"
     create_class: "Створити клас"
-    select_all_courses: "Вибрати 'Всі курси' для 50% знижки!"
+    select_all_courses: "Виберіть 'Всі курси' для 50% знижки!"
     all_courses: "Всі курси"
     number_students: "Кількість студентів"
     enter_number_students: "Введіть кількість студентів, потрібних для вашого класу."
@@ -914,12 +914,12 @@
     free_trial: "Безкоштовний пробний період для вчителів!"
     get_access: "щоб отримати особистий доступ до всіх курсів."
     questions: "Питання?"
-    faq: "FAQ по курсам"
+    faq: "ЧаПи з приводу курсів"
     question: "П:" # Like in 'Question'
     question1: "Яка відмінність між цими курсами і звичайною одиночною грою?"
     answer: "В:" # Like in 'Answer'
-    answer1: "Одиночна гра розроблена для одиночного проходження, а курси для класів."
-    answer2: "У одиночній грі є предмети, самоцвіти, вибір героїв, прокачування рівнів і внутрішньо-ігрові покупки. У курсах є управління класом і ухил, щоб студенти зосереджувалися на своєму прогресі."
+    answer1: "Самітна розроблена для проходження на одинці, а курси для проходження класами."
+    answer2: "У самітній грі є предмети, самоцвіти, вибір героїв, прокачування рівнів і внутрішньо-ігрові покупки. У курсах є управління класом і ухил, щоб студенти зосереджувалися на своєму прогресі."
     teachers_click: "Вчителям сюди"
     students_click: "Студентам сюди"
     courses_on_coco: "Курси на CodeCombat"
@@ -1043,7 +1043,7 @@
     achievement_query_misc: "Ключове досягнення випадає з різного"
     achievement_query_goals: "Ключове досягнення випадає з цілей рівня"
     level_completion: "Рівень завершено"
-    pop_i18n: "Додати I18N"
+    pop_i18n: "Додати локалізації"
     tasks: "Завдання"
     clear_storage: "Очистити свої локальні зміни"
     add_system_title: "Додати системи на рівень"
@@ -1227,7 +1227,7 @@
     recently_played: "Нещодавні ігри"
     no_recent_games: "Упродовж останніх двох тижнів не зіграно жодної гри."
     payments: "Платежі"
-#    prepaid_codes: "Prepaid Codes"
+    prepaid_codes: "Передплачені коди"
     purchased: "Придбано"
     sale: "Розпродаж"
     subscription: "Підписка"
@@ -1244,7 +1244,7 @@
     active_until: "Активна до"
     cost: "Ціна"
     next_payment: "Наступний платіж"
-    card: "Карта"
+    card: "Картка"
     status_unsubscribed_active: "У Вас немає передплати і рахунок Вам не прийде, але Ваш акаунт і далі дійсний."
     status_unsubscribed: "Отримайте доступ до новних рівнів, героїв та бонусів з абонементом CodeCombat!"

@@ -1253,13 +1253,13 @@
     declined: "Вашу картку відхилено"
     invalid_amount: "Будь ласка, введіть суму в доларах США."
     not_logged_in: "Увійдіть або створіть акаунт, щоб мати доступ до інвойсів."
-    pay: "Оплатити інвойс"
-    purchasing: "Оплата..."
+    pay: "Оплатити накладну"
+    purchasing: "Триває оплата..."
     retrying: "Помилка сервера, повторна спроба."
     success: "Успішно оплачено. Дякуємо!"

   account_prepaid:
-#    purchase_code: "Purchase a Subscription Code"
+    purchase_code: "Придбати код підписки"
 #    purchase_code1: "Subscription Codes can be redeemed to add premium subscription time to one or more CodeCombat accounts."
 #    purchase_code2: "Each CodeCombat account can only redeem a particular Subscription Code once."
 #    purchase_code3: "Subscription Code months will be added to the end of any existing subscription on the account."
@@ -1267,15 +1267,15 @@
     months: "Місяці"
     purchase_total: "Загалом"
 #    purchase_button: "Submit Purchase"
-#    your_codes: "Your Codes"
+    your_codes: "Ваші коди"
 #    redeem_codes: "Redeem a Subscription Code"
 #    prepaid_code: "Prepaid Code"
 #    lookup_code: "Lookup prepaid code"
     apply_account: "Застосувати до свого екаунту"
-#    copy_link: "You can copy the code's link and send it to someone."
+    copy_link: "Ви можете скопіювати посилання на код та надіслати будь-кому."
     quantity: "Кількіть"
 #    redeemed: "Redeemed"
-#    no_codes: "No codes yet!"
+    no_codes: "Коди відсутні!"

   loading_error:
     could_not_load: "Помилка завантаження з сервера"
@@ -1323,7 +1323,7 @@
     top_simulators: "Топ-моделювальники"
     source_document: "Вихідний документ"
     document: "Документ"
-    sprite_sheet: "Лист спрайтів"
+    sprite_sheet: "Електронна таблиця"
     employers: "Роботодавці"
     candidates: "Кандидатури"
     candidate_sessions: "Сесії кандидатів"
@@ -1581,7 +1581,7 @@
     filter_education_other: "Інше"
     filter_role_web_developer: "веб-розробник"
     filter_role_software_developer: "розробник програм"
-    filter_role_mobile_developer: "мобільний розробник"
+    filter_role_mobile_developer: "розробник додатків для мобільних"
     filter_experience: "Досвід"
     filter_experience_senior: "Досвідчений (Senior)"
     filter_experience_junior: "Початківець (Junior)"
@@ -1631,4 +1631,4 @@
     u_title: "Список учасників"
 #    ucp_title: "User Code Problems"
     lg_title: "Останні ігри"
-    clas: "CLAs"
+    clas: "CLA"


Fixed undefined

Fixed undefined

--- lib/GUI/atox-collabSelect.coffee
+++ lib/GUI/atox-collabSelect.coffee
@@ -92,8 +92,8 @@
     for i in @aTox.collab.getCollabList()
       items.push {
         action:  'Close'
-        primary: "#{i.name}"
-        desc:    "Closes collab '#{i.name}'"
+        primary: "#{i}"
+        desc:    "Closes collab '#{i}'"
         path: i
       }


TextInput now using DomElementFactories - so much cleaner! Also, think I fixed the re-focus canvas after blur bug - it wasn't working if the TextInput was removed from the tree before the blur event happened.

--- src/art/engine/forms/text_input.coffee
+++ src/art/engine/forms/text_input.coffee
@@ -6,6 +6,7 @@

 {color} = Atomic
 {createElementFromHtml} = Foundation.Browser.Dom
+{TextArea, Input} = Foundation.Browser.DomElementFactories
 {log, merge, select, inspect, createWithPostCreate} = Foundation

 module.exports = createWithPostCreate class TextInput extends SynchronizedDomOverlay
@@ -21,54 +22,46 @@
   #     maxlength:  10
   constructor: (options = {}) ->
     props = select options, "placeholder", "type", "autocapitalize", "autocomplete", "autocorrect", "maxlength"
-    tagType = if props.type == "textarea"
+    Factory = if props.type == "textarea"
       delete props.type
-      "textarea"
+      TextArea
     else
       props.type ||= 'text'
-      "input"
+      Input

-    propsString = (for k, v of props
-      "#{k}=#{inspect v}"
-    ).join " "
-    options.domElement = el = createElementFromHtml("<#{tagType} #{propsString}'></input>")
-    el.value = options.value || ""
-    style = merge options.style,
-      padding: "#{options.padding || 5}px"
-      border: '0px'
-      color: color(options.color || "black").toString()
-      padding: "0"
-      margin: "0"
-      "vertical-align": "bottom"
-      'text-align': options.align || "left"
-      'font-size': "#{options.fontSize || 16}px"
-      'background-color': 'transparent'
-      'font-family': options.fontFamily || "Arial"
-    for k, v of style
-      el.style[k] = v
+    options.domElement = Factory props,
+      options.attrs
+      options.style
+      value: options.value || ""
+      style:
+        backgroundColor:  'transparent'
+        border:           '0px'
+        color:            color(options.color || "black").toString()
+        fontFamily:       options.fontFamily || "Arial"
+        fontSize:         "#{options.fontSize || 16}px"
+        margin:           "0"
+        outline:          "0"
+        padding:          "0"
+        textAlign:        options.align || "left"
+        verticalAlign:    "bottom"
+      on:
+        keydown:  (keyboardEvent) => @getCanvasElement()?.keyDownEvent keyboardEvent
+        keyup:    (keyboardEvent) => @getCanvasElement()?.keyUpEvent keyboardEvent
+        change:   (event) => @checkIfValueChanged()
+        input:    (event) => @checkIfValueChanged()
+        select:   (event) => @queueEvent "selectionChanged"
+        focus:    (event) =>
+          log "text input focus"
+          @_canvasElementToFocusOnBlur = @getCanvasElement()
+          @_focus()
+        blur:     (event) =>
+          # since the Input element is not a child of Canvas, blur won't restore focus to the Canvas
+          @_canvasElementToFocusOnBlur?.focusCanvas() if document.body == document.activeElement
+          @_blur()

-    if options.attrs
-      for k,v of options.attrs
-        options.domElement.attr k, v
     super

     @lastValue = @value
-
-    @_addKeyboardEventListeners()
-
-    @domElement.onchange = (event) => @checkIfValueChanged()
-    @domElement.oninput  = (event) => @checkIfValueChanged()
-    @domElement.onselect = (event) => @queueEvent "selectionChanged"
-    @domElement.onblur   = (event) =>
-      # since the Input element is not a child of Canvas, blur won't restore focus to the Canvas
-      @getCanvasElement()?.focusCanvas() if document.body == document.activeElement
-      @_blur()
-
-    @domElement.onfocus  = (event) => @_focus()
-
-  _addKeyboardEventListeners: ->
-    @domElement.addEventListener "keydown", (keyboardEvent) => @getCanvasElement()?.keyDownEvent keyboardEvent
-    @domElement.addEventListener "keyup",   (keyboardEvent) => @getCanvasElement()?.keyUpEvent keyboardEvent

   preprocessEventHandlers: (handlerMap) ->
     merge super,


Improved function parsing regex

Improved function parsing regex

--- framer/Animators/Curves.coffee
+++ framer/Animators/Curves.coffee
@@ -56,7 +56,8 @@
 parseFunction = (string) ->
 	return null unless _.isString string

-	regex = /.*(Spring|Bezier)(?:\(\s*{?([a-zA-Z\d:\s,.]*)}?\s*\)|\.(\w+))?/
+	# This parses functions with or without arguments like Bezier(0.1, 0.2, 0.3, 0.4) and Spring(damping: 0.5)
+	regex = /.*(Spring|Bezier)(?:\(\s*{?([\w:\s,.]*)}?\s*\)|\.(\w+))?/
 	matches = regex.exec(string)
 	return null unless matches?
 	[match, type, args, prop] = matches
@@ -70,7 +71,7 @@
 		result.arguments = []

-	argumentsRegex = /\s*([a-zA-Z]+)\s*:\s*([\d.]+)\s*,?/g
+	argumentsRegex = /\s*([\w]+)\s*:\s*([\d.]+)\s*,?/g
 	argumentObject = {}
 	while matches = argumentsRegex.exec(args)
 		[match, property, value] = matches


Do not use an Atom file in fixtures.

Do not use an Atom file in fixtures.

It makes things confusing when doing a global search.

--- benchmark/fixtures/medium.coffee
+++ benchmark/fixtures/medium.coffee
@@ -1,368 +1,240 @@
-{View, $$} = require 'space-pen'
-AceOutdentAdaptor = require 'ace-outdent-adaptor'
-Buffer = require 'buffer'
-Cursor = require 'cursor'
-Gutter = require 'gutter'
-Renderer = require 'renderer'
-Point = require 'point'
-Range = require 'range'
-Selection = require 'selection'
-UndoManager = require 'undo-manager'
-
-$ = require 'jquery'
-_ = require 'underscore'
-
-module.exports =
-class Editor extends View
-  @content: ->
-    @div class: 'editor', tabindex: -1, =>
-      @div class: 'content', outlet: 'content', =>
-        @subview 'gutter', new Gutter
-        @div class: 'horizontal-scroller', outlet: 'horizontalScroller', =>
-          @div class: 'lines', outlet: 'lines', =>
-            @input class: 'hidden-input', outlet: 'hiddenInput'
-
-  vScrollMargin: 2
-  hScrollMargin: 10
-  softWrap: false
-  lineHeight: null
-  charWidth: null
-  charHeight: null
-  cursor: null
-  selection: null
-  buffer: null
-  highlighter: null
-  renderer: null
-  undoManager: null
-  autoIndent: null
-
-  initialize: () ->
-    requireStylesheet 'editor.css'
-    requireStylesheet 'theme/twilight.css'
-    @bindKeys()
-    @buildCursorAndSelection()
-    @handleEvents()
-    @setBuffer(new Buffer)
-    @autoIndent = true
-
-  bindKeys: ->
-    window.keymap.bindKeys '*:not(.editor *)',
-      'meta-s': 'save'
-      right: 'move-right'
-      left: 'move-left'
-      down: 'move-down'
-      up: 'move-up'
-      'shift-right': 'select-right'
-      'shift-left': 'select-left'
-      'shift-up': 'select-up'
-      'shift-down': 'select-down'
-      enter: 'newline'
-      backspace: 'backspace'
-      'delete': 'delete'
-      'meta-x': 'cut'
-      'meta-c': 'copy'
-      'meta-v': 'paste'
-      'meta-z': 'undo'
-      'meta-Z': 'redo'
-      'alt-meta-w': 'toggle-soft-wrap'
-      'alt-meta-f': 'fold-selection'
-
-    @on 'save', => @save()
-    @on 'move-right', => @moveCursorRight()
-    @on 'move-left', => @moveCursorLeft()
-    @on 'move-down', => @moveCursorDown()
-    @on 'move-up', => @moveCursorUp()
-    @on 'select-right', => @selectRight()
-    @on 'select-left', => @selectLeft()
-    @on 'select-up', => @selectUp()
-    @on 'select-down', => @selectDown()
-    @on 'newline', =>  @insertText("\n")
-    @on 'backspace', => @backspace()
-    @on 'delete', => @delete()
-    @on 'cut', => @cutSelection()
-    @on 'copy', => @copySelection()
-    @on 'paste', => @paste()
-    @on 'undo', => @undo()
-    @on 'redo', => @redo()
-    @on 'toggle-soft-wrap', => @toggleSoftWrap()
-    @on 'fold-selection', => @foldSelection()
-
-  buildCursorAndSelection: ->
-    @cursor = new Cursor(this)
-    @lines.append(@cursor)
-
-    @selection = new Selection(this)
-    @lines.append(@selection)
-
-  handleEvents: ->
-    @on 'focus', =>
-      @hiddenInput.focus()
-      false
-
-    @on 'mousedown', '.fold-placeholder', (e) =>
-      @destroyFold($(e.currentTarget).attr('foldId'))
-      false
-
-    @on 'mousedown', (e) =>
-      clickCount = e.originalEvent.detail
-
-      if clickCount == 1
-        @setCursorScreenPosition @screenPositionFromMouseEvent(e)
-      else if clickCount == 2
-        @selection.selectWord()
-      else if clickCount >= 3
-        @selection.selectLine()
-
-      @selectTextOnMouseMovement()
-
-    @hiddenInput.on "textInput", (e) =>
-      @insertText(e.originalEvent.data)
-
-    @on 'cursor:position-changed', =>
-      @hiddenInput.css(@pixelPositionForScreenPosition(@cursor.getScreenPosition()))
-
-    @one 'attach', =>
-      @calculateDimensions()
-      @hiddenInput.width(@charWidth)
-      @setMaxLineLength() if @softWrap
-      @focus()
-
-  selectTextOnMouseMovement: ->
-    moveHandler = (e) => @selectToScreenPosition(@screenPositionFromMouseEvent(e))
-    @on 'mousemove', moveHandler
-    $(document).one 'mouseup', => @off 'mousemove', moveHandler
-
-  buildLineElement: (screenLine) ->
-    { tokens } = screenLine
-    charWidth = @charWidth
-    charHeight = @charHeight
-    $$ ->
-      @div class: 'line', =>
-        appendNbsp = true
-        for token in tokens
-          if token.type is 'fold-placeholder'
-            @span '   ', class: 'fold-placeholder', style: "width: #{3 * charWidth}px; height: #{charHeight}px;", 'foldId': token.fold.id, =>
-              @div class: "ellipsis", => @raw "…"
-          else
-            appendNbsp = false
-            @span { class: token.type.replace('.', ' ') }, token.value
-        @raw ' ' if appendNbsp
-
-  renderLines: ->
-    @lines.find('.line').remove()
-    for screenLine in @getScreenLines()
-      @lines.append @buildLineElement(screenLine)
-
-  getScreenLines: ->
-    @renderer.getLines()
-
-  linesForRows: (start, end) ->
-    @renderer.linesForRows(start, end)
-
-  screenLineCount: ->
-    @renderer.lineCount()
-
-  lastRow: ->
-    @screenLineCount() - 1
-
-  setBuffer: (@buffer) ->
-    @renderer = new Renderer(@buffer)
-    @undoManager = new UndoManager(@buffer)
-    @renderLines()
-    @gutter.renderLineNumbers(@getScreenLines())
-
-    @setCursorScreenPosition(row: 0, column: 0)
-
-    @buffer.on 'change', (e) =>
-      @cursor.bufferChanged(e)
-
-    @renderer.on 'change', (e) =>
-      @gutter.renderLineNumbers(@getScreenLines())
-
-      @cursor.refreshScreenPosition() unless e.bufferChanged
-      { oldRange, newRange } = e
-      screenLines = @linesForRows(newRange.start.row, newRange.end.row)
-      if newRange.end.row > oldRange.end.row
-        # update, then insert elements
-        for row in [newRange.start.row..newRange.end.row]
-          if row <= oldRange.end.row
-            @updateLineElement(row, screenLines.shift())
-          else
-            @insertLineElement(row, screenLines.shift())
-      else
-        # traverse in reverse... remove, then update elements
-        screenLines.reverse()
-        for row in [oldRange.end.row..oldRange.start.row]
-          if row > newRange.end.row
-            @removeLineElement(row)
-          else
-            @updateLineElement(row, screenLines.shift())
-
-  updateLineElement: (row, screenLine) ->
-    @getLineElement(row).replaceWith(@buildLineElement(screenLine))
-
-  insertLineElement: (row, screenLine) ->
-    newLineElement = @buildLineElement(screenLine)
-    insertBefore = @getLineElement(row)
-    if insertBefore.length
-      insertBefore.before(newLineElement)
+# **Docco** is a quick-and-dirty, hundred-line-long, literate-programming-style
+# documentation generator. It produces HTML
+# that displays your comments alongside your code. Comments are passed through
+# [Markdown](http://daringfireball.net/projects/markdown/syntax), and code is
+# passed through [Pygments](http://pygments.org/) syntax highlighting.
+# This page is the result of running Docco against its own source file.
+#
+# If you install Docco, you can run it from the command-line:
+#
+#     docco src/*.coffee
+#
+# ...will generate an HTML documentation page for each of the named source files,
+# with a menu linking to the other pages, saving it into a `docs` folder.
+#
+# The [source for Docco](http://github.com/jashkenas/docco) is available on GitHub,
+# and released under the MIT license.
+#
+# To install Docco, first make sure you have [Node.js](http://nodejs.org/),
+# [Pygments](http://pygments.org/) (install the latest dev version of Pygments
+# from [its Mercurial repo](http://dev.pocoo.org/hg/pygments-main)), and
+# [CoffeeScript](http://coffeescript.org/). Then, with NPM:
+#
+#     sudo npm install -g docco
+#
+# Docco can be used to process CoffeeScript, JavaScript, Ruby, Python, or TeX files.
+# Only single-line comments are processed -- block comments are ignored.
+#
+#### Partners in Crime:
+#
+# * If **Node.js** doesn't run on your platform, or you'd prefer a more
+# convenient package, get [Ryan Tomayko](http://github.com/rtomayko)'s
+# [Rocco](http://rtomayko.github.com/rocco/rocco.html), the Ruby port that's
+# available as a gem.
+#
+# * If you're writing shell scripts, try
+# [Shocco](http://rtomayko.github.com/shocco/), a port for the **POSIX shell**,
+# also by Mr. Tomayko.
+#
+# * If Python's more your speed, take a look at
+# [Nick Fitzgerald](http://github.com/fitzgen)'s [Pycco](http://fitzgen.github.com/pycco/).
+#
+# * For **Clojure** fans, [Fogus](http://blog.fogus.me/)'s
+# [Marginalia](http://fogus.me/fun/marginalia/) is a bit of a departure from
+# "quick-and-dirty", but it'll get the job done.
+#
+# * **Lua** enthusiasts can get their fix with
+# [Robert Gieseke](https://github.com/rgieseke)'s [Locco](http://rgieseke.github.com/locco/).
+#
+# * And if you happen to be a **.NET**
+# aficionado, check out [Don Wilson](https://github.com/dontangg)'s
+# [Nocco](http://dontangg.github.com/nocco/).
+
+#### Main Documentation Generation Functions
+
+# Generate the documentation for a source file by reading it in, splitting it
+# up into comment/code sections, highlighting them for the appropriate language,
+# and merging them into an HTML template.
+generate_documentation = (source, callback) ->
+  fs.readFile source, "utf-8", (error, code) ->
+    throw error if error
+    sections = parse source, code
+    highlight source, sections, ->
+      generate_html source, sections
+      callback()
+
+# Given a string of source code, parse out each comment and the code that
+# follows it, and create an individual **section** for it.
+# Sections take the form:
+#
+#     {
+#       docs_text: ...
+#       docs_html: ...
+#       code_text: ...
+#       code_html: ...
+#     }
+#
+parse = (source, code) ->
+  lines    = code.split '\n'
+  sections = []
+  language = get_language source
+  has_code = docs_text = code_text = ''
+
+  save = (docs, code) ->
+    sections.push docs_text: docs, code_text: code
+
+  for line in lines
+    if line.match(language.comment_matcher) and not line.match(language.comment_filter)
+      if has_code
+        save docs_text, code_text
+        has_code = docs_text = code_text = ''
+      docs_text += line.replace(language.comment_matcher, '') + '\n'
     else
-      @lines.append(newLineElement)
-
-  removeLineElement: (row) ->
-    @getLineElement(row).remove()
-
-  getLineElement: (row) ->
-    @lines.find("div.line:eq(#{row})")
-
-  toggleSoftWrap: ->
-    @setSoftWrap(not @softWrap)
-
-  setMaxLineLength: (maxLength) ->
-    maxLength ?=
-      if @softWrap
-        Math.floor(@horizontalScroller.width() / @charWidth)
-      else
-        Infinity
-
-    @renderer.setMaxLineLength(maxLength) if maxLength
-
-  createFold: (range) ->
-    @renderer.createFold(range)
-
-  setSoftWrap: (@softWrap) ->
-    @setMaxLineLength()
-    if @softWrap
-      @addClass 'soft-wrap'
-      @_setMaxLineLength = => @setMaxLineLength()
-      $(window).on 'resize', @_setMaxLineLength
-    else
-      @removeClass 'soft-wrap'
-      $(window).off 'resize', @_setMaxLineLength
-
-  save: ->
-    if not @buffer.path
-      path = $native.saveDialog()
-      return if not path
-      @buffer.path = path
-
-    @buffer.save()
-
-  clipScreenPosition: (screenPosition, options={}) ->
-    @renderer.clipScreenPosition(screenPosition, options)
-
-  pixelPositionForScreenPosition: ({row, column}) ->
-    { top: row * @lineHeight, left: column * @charWidth }
-
-  screenPositionFromPixelPosition: ({top, left}) ->
-    screenPosition = new Point(Math.floor(top / @lineHeight), Math.floor(left / @charWidth))
-
-  screenPositionForBufferPosition: (position) ->
-    @renderer.screenPositionForBufferPosition(position)
-
-  bufferPositionForScreenPosition: (position) ->
-    @renderer.bufferPositionForScreenPosition(position)
-
-  screenRangeForBufferRange: (range) ->
-    @renderer.screenRangeForBufferRange(range)
-
-  bufferRangeForScreenRange: (range) ->
-    @renderer.bufferRangeForScreenRange(range)
-
-  bufferRowsForScreenRows: ->
-    @renderer.bufferRowsForScreenRows()
-
-  screenPositionFromMouseEvent: (e) ->
-    { pageX, pageY } = e
-    @screenPositionFromPixelPosition
-      top: pageY - @horizontalScroller.offset().top
-      left: pageX - @horizontalScroller.offset().left + @horizontalScroller.scrollLeft()
-
-  calculateDimensions: ->
-    fragment = $('
x
')
-    @lines.append(fragment)
-    @charWidth = fragment.width()
-    @charHeight = fragment.find('span').height()
-    @lineHeight = fragment.outerHeight()
-    fragment.remove()
-
-
-  getCursor: -> @cursor
-  moveCursorUp: -> @cursor.moveUp()
-  moveCursorDown: -> @cursor.moveDown()
-  moveCursorRight: -> @cursor.moveRight()
-  moveCursorLeft: -> @cursor.moveLeft()
-
-  getCurrentScreenLine: -> @buffer.lineForRow(@getCursorScreenRow())
-  getCurrentBufferLine: -> @buffer.lineForRow(@getCursorBufferRow())
-  setCursorScreenPosition: (position) -> @cursor.setScreenPosition(position)
-  getCursorScreenPosition: -> @cursor.getScreenPosition()
-  setCursorBufferPosition: (position) -> @cursor.setBufferPosition(position)
-  getCursorBufferPosition: -> @cursor.getBufferPosition()
-  setCursorScreenRow: (row) -> @cursor.setScreenRow(row)
-  getCursorScreenRow: -> @cursor.getScreenRow()
-  getCursorBufferRow: -> @cursor.getBufferRow()
-  setCursorScreenColumn: (column) -> @cursor.setScreenColumn(column)
-  getCursorScreenColumn: -> @cursor.getScreenColumn()
-  setCursorBufferColumn: (column) -> @cursor.setBufferColumn(column)
-  getCursorBufferColumn: -> @cursor.getBufferColumn()
-
-  getSelection: -> @selection
-  getSelectedText: -> @selection.getText()
-  selectRight: -> @selection.selectRight()
-  selectLeft: -> @selection.selectLeft()
-  selectUp: -> @selection.selectUp()
-  selectDown: -> @selection.selectDown()
-  selectToScreenPosition: (position) -> @selection.selectToScreenPosition(position)
-  selectToBufferPosition: (position) -> @selection.selectToBufferPosition(position)
-
-  insertText: (text) ->
-    { text, shouldOutdent } = @autoIndentText(text)
-    @selection.insertText(text)
-    @autoOutdentText() if shouldOutdent
-
-  autoIndentText: (text) ->
-    if @autoIndent
-      row = @getCursorScreenPosition().row
-      state = @renderer.lineForRow(row).state
-      if text[0] == "\n"
-        indent = @buffer.mode.getNextLineIndent(state, @getCurrentBufferLine(), atom.tabText)
-        text = text[0] + indent + text[1..]
-      else if @buffer.mode.checkOutdent(state, @getCurrentBufferLine(), text)
-        shouldOutdent = true
-
-    {text, shouldOutdent}
-
-  autoOutdentText: ->
-    screenRow = @getCursorScreenPosition().row
-    bufferRow = @getCursorBufferPosition().row
-    state = @renderer.lineForRow(screenRow).state
-    @buffer.mode.autoOutdent(state, new AceOutdentAdaptor(@buffer, this), bufferRow)
-
-  cutSelection: -> @selection.cut()
-  copySelection: -> @selection.copy()
-  paste: -> @insertText($native.readFromPasteboard())
-
-  foldSelection: -> @selection.fold()
-
-  backspace: ->
-    @selectLeft() if @selection.isEmpty()
-    @selection.delete()
-
-  delete: ->
-    @selectRight() if @selection.isEmpty()
-    @selection.delete()
-
-  undo: ->
-    @undoManager.undo()
-
-  redo: ->
-    @undoManager.redo()
-
-  destroyFold: (foldId) ->
-    fold = @renderer.foldsById[foldId]
-    fold.destroy()
-    @setCursorBufferPosition(fold.start)
-
-  logLines: ->
-    @renderer.logLines()
+      has_code = yes
+      code_text += line + '\n'
+  save docs_text, code_text
+  sections
+
+# Highlights a single chunk of CoffeeScript code, using **Pygments** over stdio,
+# and runs the text of its corresponding comment through **Markdown**, using
+# [Showdown.js](http://attacklab.net/showdown/).
+#
+# We process the entire file in a single call to Pygments by inserting little
+# marker comments between each section and then splitting the result string
+# wherever our markers occur.
+highlight = (source, sections, callback) ->
+  language = get_language source
+  pygments = spawn 'pygmentize', ['-l', language.name, '-f', 'html', '-O', 'encoding=utf-8,tabsize=2']
+  output   = ''
+
+  pygments.stderr.addListener 'data',  (error)  ->
+    console.error error.toString() if error
+
+  pygments.stdin.addListener 'error',  (error)  ->
+    console.error "Could not use Pygments to highlight the source."
+    process.exit 1
+
+  pygments.stdout.addListener 'data', (result) ->
+    output += result if result
+
+  pygments.addListener 'exit', ->
+    output = output.replace(highlight_start, '').replace(highlight_end, '')
+    fragments = output.split language.divider_html
+    for section, i in sections
+      section.code_html = highlight_start + fragments[i] + highlight_end
+      section.docs_html = showdown.makeHtml section.docs_text
+    callback()
+
+  if pygments.stdin.writable
+    pygments.stdin.write((section.code_text for section in sections).join(language.divider_text))
+    pygments.stdin.end()
+
+# Once all of the code is finished highlighting, we can generate the HTML file
+# and write out the documentation. Pass the completed sections into the template
+# found in `resources/docco.jst`
+generate_html = (source, sections) ->
+  title = path.basename source
+  dest  = destination source
+  html  = docco_template {
+    title: title, sections: sections, sources: sources, path: path, destination: destination
+  }
+  console.log "docco: #{source} -> #{dest}"
+  fs.writeFile dest, html
+
+#### Helpers & Setup
+
+# Require our external dependencies, including **Showdown.js**
+# (the JavaScript implementation of Markdown).
+fs       = require 'fs'
+path     = require 'path'
+showdown = require('./../vendor/showdown').Showdown
+{spawn, exec} = require 'child_process'
+
+# A list of the languages that Docco supports, mapping the file extension to
+# the name of the Pygments lexer and the symbol that indicates a comment. To
+# add another language to Docco's repertoire, add it here.
+languages =
+  '.coffee':
+    name: 'coffee-script', symbol: '#'
+  '.js':
+    name: 'javascript', symbol: '//'
+  '.rb':
+    name: 'ruby', symbol: '#'
+  '.py':
+    name: 'python', symbol: '#'
+  '.tex':
+    name: 'tex', symbol: '%'
+  '.latex':
+    name: 'tex', symbol: '%'
+  '.c':
+    name: 'c', symbol: '//'
+  '.h':
+    name: 'c', symbol: '//'
+
+# Build out the appropriate matchers and delimiters for each language.
+for ext, l of languages
+
+  # Does the line begin with a comment?
+  l.comment_matcher = new RegExp('^\\s*' + l.symbol + '\\s?')
+
+  # Ignore [hashbangs](http://en.wikipedia.org/wiki/Shebang_(Unix\))
+  # and interpolations...
+  l.comment_filter = new RegExp('(^#![/]|^\\s*#\\{)')
+
+  # The dividing token we feed into Pygments, to delimit the boundaries between
+  # sections.
+  l.divider_text = '\n' + l.symbol + 'DIVIDER\n'
+
+  # The mirror of `divider_text` that we expect Pygments to return. We can split
+  # on this to recover the original sections.
+  # Note: the class is "c" for Python and "c1" for the other languages
+  l.divider_html = new RegExp('\\n*' + l.symbol + 'DIVIDER<\\/span>\\n*')
+
+# Get the current language we're documenting, based on the extension.
+get_language = (source) -> languages[path.extname(source)]
+
+# Compute the destination HTML path for an input source file path. If the source
+# is `lib/example.coffee`, the HTML will be at `docs/example.html`
+destination = (filepath) ->
+  'docs/' + path.basename(filepath, path.extname(filepath)) + '.html'
+
+# Ensure that the destination directory exists.
+ensure_directory = (dir, callback) ->
+  exec "mkdir -p #{dir}", -> callback()
+
+# Micro-templating, originally by John Resig, borrowed by way of
+# [Underscore.js](http://documentcloud.github.com/underscore/).
+template = (str) ->
+  new Function 'obj',
+    'var p=[],print=function(){p.push.apply(p,arguments);};' +
+    'with(obj){p.push(\'' +
+    str.replace(/[\r\t\n]/g, " ")
+       .replace(/'(?=[^<]*%>)/g,"\t")
+       .split("'").join("\\'")
+       .split("\t").join("'")
+       .replace(/<%=(.+?)%>/g, "',$1,'")
+       .split('<%').join("');")
+       .split('%>').join("p.push('") +
+       "');}return p.join('');"
+
+# Create the template that we will use to generate the Docco HTML page.
+docco_template  = template fs.readFileSync(__dirname + '/../resources/docco.jst').toString()
+
+# The CSS styles we'd like to apply to the documentation.
+docco_styles    = fs.readFileSync(__dirname + '/../resources/docco.css').toString()
+
+# The start of each Pygments highlight block.
+highlight_start = '

'
+
+# The end of each Pygments highlight block.
+highlight_end   = '

'
+
+# Run the script.
+# For each source file passed in as an argument, generate the documentation.
+sources = process.ARGV.sort()
+if sources.length
+  ensure_directory 'docs', ->
+    fs.writeFile 'docs/docco.css', docco_styles
+    files = sources.slice(0)
+    next_file = -> generate_documentation files.shift(), next_file if files.length
+    next_file()


Skip fallback.

Skip fallback.

--- src/coffee/producttypegenerator.coffee
+++ src/coffee/producttypegenerator.coffee
@@ -86,7 +86,7 @@
           label: @_i18n row, ATTRIBUTE_LABEL
           type:
             name: row[ATTRIBUTE_TYPE]
-          attributeConstraint: if row[ATTRIBUTE_CONSTRAINT] then row[ATTRIBUTE_CONSTRAINT] else 'None'
+          attributeConstraint: row[ATTRIBUTE_CONSTRAINT]
           isRequired: row[ATTRIBUTE_IS_REQUIRED] is 'true'
           isSearchable: row[ATTRIBUT_IS_SEARCHABLE] is 'true'


Add operators for ===, ==, := for ligature rendering (#785)


Clean-up half-implemented inline markers

The TypeChecking class has some partial support for inline markers.
This commit cleans that out in preparation for moving the handling to
separate classes.

--- lib/features/typechecking.coffee
+++ lib/features/typechecking.coffee
@@ -1,35 +1,15 @@
 {MessagePanelView, LineMessageView} = require 'atom-message-panel'
 _ = require 'lodash'
 {log, isScalaSource} = require '../utils'
-{CompositeDisposable} = require 'atom'

-# TODO: Add a map from file -> array[messages] and editor subs
 module.exports =
 class TypeChecking
-

   constructor: ->
     @messages = new MessagePanelView
       title: 'Ensime'
     @messages.attach()

-    @disposables = new CompositeDisposable
-
-    @editors = new Map
-
-    @markersOfFile = new Map
-
-    @disposables.add atom.workspace.observeTextEditors (editor) =>
-      if isScalaSource(editor) # TODO: check that the source is also in an ensime-activated project.  ensime:start should log which directories are ensime-enabled
-        @editors.set(editor.getPath(), editor)
-        @disposables.add editor.onDidDestroy () =>
-          @editors.delete(editor.getPath())
-
-  hide: ->
-    @messages.hide()
-
-  show: ->
-    @messages.show()

   addScalaNotes: (msg) ->
     notes = msg.notes
@@ -53,13 +33,6 @@
       if(not file.includes('dep-src')) # TODO: put under flag
         addNoteToMessageView note for note in notes

-        # TODO: add markers if editor open
-        if(@editors.has(file))
-          editor = @editors.get(file)
-          for note in notes
-            marker = editor.markBufferPosition([note.line, note.col], {invalidate: 'inside'})
-            editor.decorateMarker(marker, {})
-

   clearScalaNotes: ->
     @messages.clear()
@@ -69,8 +42,3 @@
     @messages.clear()
     @messages?.close()
     @messages = null
-
-    @markersOfFile = null
-    @editors = null
-
-    @disposables.dispose()


Fix jumpy hyperlinks; Make summary link do an inspect by default

--- src/ext/components/frame-output.coffee
+++ src/ext/components/frame-output.coffee
@@ -35,7 +35,8 @@
         ]
       ]

-    $('a.action', el).click ->
+    $('a.action', el).click (e) ->
+      e.preventDefault()
       $link = $ @
       action = $link.attr 'data-action'
       index = parseInt ($link.attr 'data-index'), 10
@@ -44,8 +45,7 @@
           if index >= 0
             row = data.rows[index]
             if row
-              _.insertAndExecuteCell 'cs', "getColumnSummary #{stringify _frame.key.name}, #{stringify row.label}"
-      return no
+              _.insertAndExecuteCell 'cs', "inspect getColumnSummary #{stringify _frame.key.name}, #{stringify row.label}"

     el


probably have a good way of resetting delay

--- browser/coffee/controller.coffee
+++ browser/coffee/controller.coffee
@@ -24,7 +24,9 @@

 do open = (delay = 1000) ->
     connection = new WebSocket('ws://localhost:8886/controls')
-    connection.onopen = -> console.log 'connected to controls'
+    connection.onopen = ->
+        delay = 1000
+        console.log 'connected to controls'
     connection.onmessage = ({data}) -> press data
     connection.onclose = -> setTimeout ->
         newDelay = delay * 1.5


add clone to avoid vue define property again

--- browser/src/coffee/search.coffee
+++ browser/src/coffee/search.coffee
@@ -103,7 +103,7 @@
                     lt[i.id] = true
                     i.docset =
                         name: result.docset.name
-                    S.vm.$data.results.push i
+                    S.vm.$data.results.push _.clone(i)
             handle.on 'finish', (allData)->
                 return if allData.length < 1
                 return unless allData[0].keyword is S.vm.$data.keyword


remove redundant '$ -> ' from findingaids.js.coffee

--- app/assets/javascripts/findingaids.js.coffee
+++ app/assets/javascripts/findingaids.js.coffee
@@ -3,7 +3,6 @@
   if $("#repository_header").html() != ""
     $("#search_field").hide()

-$ ->
   $('a[data-remote=true]').click ->
     $('#ajax-modal .modal-content').html($('<div />').addClass('modal-header').append($('<h3 />').addClass('modal-title').html($(this).html())))
     $('#ajax-modal .modal-header').prepend($('<button />').attr({type: 'button', class: 'close', 'data-dismiss': 'modal'}).html('×'))


misc cleanup

misc cleanup

--- public/dreamcatcher/controllers/entries_controller.coffee
+++ public/dreamcatcher/controllers/entries_controller.coffee
@@ -32,9 +32,13 @@
     if entry?
       if entry['type'] is currentEntryType  # make sure our cookie entry type matches new or edit like the current form
         currentEntryId = parseInt($('#entryId').data('id')) if currentEntryType is 'edit'
-
-      @populateForm(entry) unless currentEntryType is 'edit' and currentEntryId isnt entry['id']
-      @clearState()
+
+      if currentEntryType is 'edit' and currentEntryId isnt entry['id']
+        @stateRetrieved = false
+      else
+        @populateForm(entry)
+        @stateRetrieved = true
+        @clearState()

       log "entry[type] #{entry['type']} entry[id]: #{entry['id']} currentEntryType: #{currentEntryType} currentEntryId: #{currentEntryId}"

@@ -42,17 +46,13 @@
   # populate form with saved state so they can see it, then confirm they want to use it
   populateForm: (entry) ->
     $(field).val entry[field] for field in @fields
-    $('#currentImages').html entry['#currentImages']
-    @stateRetrieved = true
+    $('#currentImages').html entry['#currentImages']
     @clearForm() unless confirm 'you have an unsaved entry.\n\nwould you like to use it?' # reset form unless confirmation
-
-
+
   clearForm: ->
-    log 'clearing form'
     $(field).val '' for field in @fields
     $('#currentImages').html ''
-    @stateRetrieved = false
-
+
   clearState: ->
     @entryCookie.clear()


neater filter

neater filter

--- urb/zod/pub/work/src/js/components/FilterComponent.coffee
+++ urb/zod/pub/work/src/js/components/FilterComponent.coffee
@@ -14,45 +14,24 @@
     txt = $t.find('.input').text().trim()
     key = $t.attr('data-key')
     if txt.length is 0 then txt = null
-    if key is 'audience' then txt = txt.split " "
-    if key is 'tags' then txt = [txt]
+    else switch key
+      when 'audience' then txt = txt.split " "
+      when 'tags'     then txt = [txt]
     @props.onChange key,txt
+
+  fields: [ {filter:'owned',   key:'owner',    title: 'Owned by:'},
+            {filter:'tag',     key:'tags',     title: 'Tag:'},
+            {filter:'channel', key:'audience', title: 'Audience:'},
+            {filter:'status',  key:'status',   title: 'Status:'} ]
+
   render: ->
-    (div {className:'filters'}, [
-      (div {className:'owned filter ib','data-key':'owner'}, [
-        (label {}, 'Owned by:')
+    (div {className:'filters'}, @fields.map ({filter,key,title})=>
+      (div {key, 'data-key':key, className:"#{filter} filter ib"}, [
+        (label {}, title)
         (div {
             contentEditable:true
             className:'input ib'
             onKeyDown:@_onKeyDown
             onBlur:@_onBlur
-            },@props.filters.owned)
-      ])
-      (div {className:'tag filter ib','data-key':'tags'}, [
-        (label {}, 'Tag:')
-        (div {
-            contentEditable:true
-            className:'input ib'
-            onKeyDown:@_onKeyDown
-            onBlur:@_onBlur
-            },@props.filters.tag)
-      ])
-      (div {className:'channel filter ib','data-key':'audience'}, [
-        (label {}, 'Audience:')
-        (div {
-            contentEditable:true
-            className:'input ib'
-            onKeyDown:@_onKeyDown
-            onBlur:@_onBlur
-            },@props.filters.channel)
-      ])
-      (div {className:'status filter ib','data-key':'status'}, [
-        (label {}, 'Status:')
-        (div {
-            contentEditable:true
-            className:'input ib'
-            onKeyDown:@_onKeyDown
-            onBlur:@_onBlur
-            },@props.filters.status)
-      ])
-    ])+            },@props.filters[filter])
+      ]))


Add frames to animator

Add frames to animator

--- src/coffee/Animator.coffee
+++ src/coffee/Animator.coffee
@@ -1,5 +1,6 @@
+Frame = require './Frame'
+
 # A magician with a dream
-
 Animator =
   # Gets all the frames needed
   getAnimation: ->
@@ -39,14 +40,31 @@

   # Verse 1
   _getVerse1: ->
+    moment = new Frame.moment().getPatterns()
+    thought = new Frame.thought().getPatterns()
+    dream = new Frame.dream().getPatterns()
+    wish = new Frame.wish().getPatterns()
+    search = new Frame.search().getPatterns()
     return []

   # Verse 2
   _getVerse2: ->
+    drink = new Frame.drink().getPatterns()
+    text = new Frame.text().getPatterns()
+    laugh = new Frame.laugh().getPatterns()
+    flirt = new Frame.flirt().getPatterns()
+    kiss = new Frame.kiss().getPatterns()
+    lie = new Frame.lie().getPatterns()
+    breakup = new Frame.breakup().getPatterns()
     return []

   # Verse 3
   _getVerse3: ->
+    game = new Frame.game().getPatterns()
+    success = new Frame.success().getPatterns()
+    failure = new Frame.failure().getPatterns()
+    better = new Frame.better().getPatterns()
+    year = new Frame.year().getPatterns()
     return []

 module.exports = Animator


Make core:copy work as expected

Make core:copy work as expected

* When nothing is selected, copy document as HTML
* When something is selected, copy selection as plain text

--- lib/markdown-preview-view.coffee
+++ lib/markdown-preview-view.coffee
@@ -13,7 +13,7 @@

   constructor: ({@editorId, @filePath}) ->
     @element = document.createElement('div')
-    @element.classList.add('markdown-preview', 'native-key-bindings')
+    @element.classList.add('markdown-preview')
     @element.tabIndex = -1
     @emitter = new Emitter
     @loaded = false
@@ -111,11 +111,12 @@
       @disposables.add atom.grammars.onDidUpdateGrammar -> lazyRenderMarkdown()

     atom.commands.add @element,
-      'core:save-as': (event) =>
+      'core:save-as': =>
         event.stopPropagation()
         @saveAs()
-      'core:copy': (event) =>
-        event.stopPropagation() if @copyToClipboard()
+      'core:copy': =>
+        event.stopPropagation()
+        @copyToClipboard()
       'markdown-preview:zoom-in': =>
         zoomLevel = parseFloat(getComputedStyle(@element).zoom)
         @element.style.zoom = zoomLevel + 0.1
@@ -269,22 +270,21 @@
     @element.appendChild(div)

   copyToClipboard: ->
-    return false if @loading
+    return if @loading

     selection = window.getSelection()
     selectedText = selection.toString()
     selectedNode = selection.baseNode

     # Use default copy event handler if there is selected text inside this view
-    return false if selectedText and selectedNode? and (@element is selectedNode or @element.contains(selectedNode))
-
-    @getHTML (error, html) ->
-      if error?
-        console.warn('Copying Markdown as HTML failed', error)
-      else
-        atom.clipboard.write(html)
-
-    true
+    if selectedText and selectedNode? and (@element is selectedNode or @element.contains(selectedNode))
+      atom.clipboard.write(selectedText)
+    else
+      @getHTML (error, html) ->
+        if error?
+          console.warn('Copying Markdown as HTML failed', error)
+        else
+          atom.clipboard.write(html)

   saveAs: ->
     return if @loading


Use relative path for extension require

Use relative path for extension require

--- lib/markdown-preview-view.coffee
+++ lib/markdown-preview-view.coffee
@@ -1,7 +1,7 @@
 {_, $, $$$, EditorView, ScrollView} = require 'atom'
 path = require 'path'
 roaster = require 'roaster'
-{extensionForFenceName} = require 'extension-helper'
+{extensionForFenceName} = require './extension-helper'

 module.exports =
 class MarkdownPreviewView extends ScrollView


href attribute, not id

href attribute, not id

Signed-off-by: Steven Hoffman <fe20d0b9f22d8cad9ff6cecdb579c0c0dbedec8a@me.com>

--- vendor/assets/javascripts/fustrate/components/tabs.coffee
+++ vendor/assets/javascripts/fustrate/components/tabs.coffee
@@ -6,7 +6,7 @@
       @activateTab $(e.currentTarget)

     if window.location.hash
-      @activateTab $("li > a#{window.location.hash}", @tabs).first()
+      @activateTab $("li > a[href=#{window.location.hash}]", @tabs).first()
     else if $('li > a.active', @tabs).length > 0
       @activateTab $('li > a.active', @tabs).first()
     else


Fix popup prevent default.

Fix popup prevent default.

--- vendor/assets/javascripts/jquery.drappers.popup.js.coffee
+++ vendor/assets/javascripts/jquery.drappers.popup.js.coffee
@@ -24,6 +24,7 @@
     show: ->
       @hideAnotherPopups()
       @element.show()
+      @element.click (e)-> e.preventDefault()
       $('body').click => @hide()

     hideAnotherPopups: ->


Adds title to guide, for hover tip.

Adds title to guide, for hover tip.

--- lib/multi-wrap-guide-view.coffee
+++ lib/multi-wrap-guide-view.coffee
@@ -261,6 +261,7 @@
   # Private: Sets the tip text to be the given column.
   # Pre-condition: The tip element must be appended to the guide.
   setTipColumn: (guide, column) ->
+    guide.prop 'title', column
     guide.find('div.multi-wrap-guide-tip').text column

   # Private: Returns a `scopeSelector` for `atom.config.set()`; compare to


Fire an onDidNoopSearch event when a cached search is used

--- lib/project/results-model.coffee
+++ lib/project/results-model.coffee
@@ -48,6 +48,9 @@
   onDidErrorForPath: (callback) ->
     @emitter.on 'did-error-for-path', callback

+  onDidNoopSearch: (callback) ->
+    @emitter.on 'did-noop-search', callback
+
   onDidStartSearching: (callback) ->
     @emitter.on 'did-start-searching', callback

@@ -110,7 +113,9 @@
       true

   search: (findPattern, pathsPattern, replacePattern, options={}) ->
-    return Promise.resolve() unless @shouldRerunSearch(findPattern, pathsPattern, replacePattern, options)
+    unless @shouldRerunSearch(findPattern, pathsPattern, replacePattern, options)
+      @emitter.emit 'did-noop-search'
+      return Promise.resolve()

     {keepReplacementState} = options
     if keepReplacementState


fixes

fixes

--- lib/browser-plus-view.coffee
+++ lib/browser-plus-view.coffee
@@ -192,8 +192,8 @@
           #               node.innerHTML='#{@model.browserPlus.bpStyle}';
           #               document.getElementsByTagName('head')[0].appendChild(node);
           #               """
-          @htmlv[0]?.executeJavaScript BrowserPlusView.loadCSS.call @,'bp-style.css',true
-          @htmlv[0]?.executeJavaScript BrowserPlusView.loadCSS.call @,'jquery.notifyBar.css',true
+          @htmlv[0]?.executeJavaScript BrowserPlusView.loadCSS.call @,'bp-style.css'
+          @htmlv[0]?.executeJavaScript BrowserPlusView.loadCSS.call @,'jquery.notifyBar.css'

       @htmlv[0]?.addEventListener "page-favicon-updated", (e)=>
         _ = require 'lodash'
@@ -492,15 +492,19 @@
   @getHotKeys: ->
     fs.readFileSync "#{@model.browserPlus.resources}/jquery.hotkeys.min.js",'utf-8'

-  @loadCSS: (filename,fullpath)->
-    debugger
-    filename = "#{@model.browserPlus.resources}/#{filename}" unless fullpath
+  @loadCSS: (filename,fullpath=false)->
+    unless fullpath
+      fpath = "file:///#{@model.browserPlus.resources.replace(/\\/g,'/')}"
+      filename = "#{fpath}#{filename}"
     """
     jQuery('head').append(jQuery('<link type="text/css" rel="stylesheet" href="#{filename}">'))
     """

   @loadJS: (filename,fullpath=false)->
-    filename = "#{@model.browserPlus.resources}/#{filename}" unless fullpath
+    unless fullpath
+      fpath = "file:///#{@model.browserPlus.resources.replace(/\\/g,'/')}"
+      filename = "#{fpath}#{filename}"
+
     """
     jQuery('head').append(jQuery('<script type="text/javascript" src="#{filename}">'))
     """


fixed name link in email notifications

fixed name link in email notifications

--- workers/social/lib/social/models/emailnotification.coffee
+++ workers/social/lib/social/models/emailnotification.coffee
@@ -99,7 +99,7 @@
     {actor, receiver, event, contents} = data

     username = receiver.getAt 'profile.nickname'
-    sender   = actor._id ? actor
+    sender   = actor._id ? actor.id ? actor
     receiver = receiver._id

     activity =


BrowserServer page loading semantics.

BrowserServer page loading semantics.

* BrowserServer now drops client events/updates when a page is loading.

* After a page has loaded, BrowserServer now re-syncs all clients.

--- lib/server/browser_server.coffee
+++ lib/server/browser_server.coffee
@@ -15,6 +15,10 @@
         @nodes = new TaggedNodeCollection()
         @events = new EventProcessor(this)

+        # Indicates whether @browser is currently loading a page.
+        # If so, we don't process client events/updates.
+        @browserLoading = false
+
         for own event, handler of DOMEventHandlers
             do (event, handler) =>
                 @browser.on event, () =>
@@ -23,10 +27,6 @@
         # TODO:
         #   'log'
         #   bandwidth measuring and logging etc should go here, since browser is protocol agnostic.
-        #   some sort of page load event so we can resync.
-        #       this should be 2 events:
-        #           'pageLoading' - stop client updates and event processing
-        #           'pageLoaded' - resumse

     broadcastEvent : (name, params) ->
         for socket in @sockets
@@ -45,18 +45,20 @@
             @sockets = (s for s in @sockets when s != socket)

     processEvent : (args ) =>
-        @broadcastEvent 'pauseRendering'
-        @events.processEvent(args)
-        @broadcastEvent 'resumeRendering'
+        if !@browserLoading
+            @broadcastEvent 'pauseRendering'
+            @events.processEvent(args)
+            @broadcastEvent 'resumeRendering'

     processClientSetAttribute : (args) =>
-        target = @nodes.get(args.target)
-        {attribute, value} = args
-        if attribute == 'src'
-            return
-        if attribute == 'selectedIndex'
-            return target[attribute] = value
-        target.setAttribute(attribute, value)
+        if !@browserLoading
+            target = @nodes.get(args.target)
+            {attribute, value} = args
+            if attribute == 'src'
+                return
+            if attribute == 'selectedIndex'
+                return target[attribute] = value
+            target.setAttribute(attribute, value)

 # The BrowserServer constructor iterates over the properties in this object and
 # adds an event handler to the Browser for each one.  The function name must
@@ -128,4 +130,18 @@
     ExitedTimer : () ->
         @broadcastEvent 'resumeRendering'

+    PageLoading : () ->
+        @browserLoading = true
+
+    PageLoaded : () ->
+        @browserLoading = false
+        cmds = serialize(@browser.window.document, @browser.resources)
+        events = @events.getSnapshot()
+        components = @browser.getSnapshot().components
+        for socket in @sockets
+            socket.emit 'loadFromSnapshot'
+                nodes      : cmds
+                events     : events
+                components : components
+
 module.exports = BrowserServer


Fix closing tab behavior

Fix closing tab behavior

--- src/components/header.cjsx
+++ src/components/header.cjsx
@@ -46,12 +46,10 @@

         {item.name}

-
+

-  _handleCloseItem: (e) =>
-    index = parseInt(e.target.value, 10)
-
+  _handleCloseItem: (index) =>
     @setState (state) ->
       state.menuItems.splice(index, 1)


removed js

removed js

--- src/javascripts/views/card_view.coffee
+++ src/javascripts/views/card_view.coffee
@@ -41,8 +41,6 @@
       @$el.find(".#{attr}").text(@model.get(attr))

     @$el.find('.name').text('◆ ' + @model.get('name')) if @model.isUnique()
-    typeEl = @$el.find('.type')
-    @$el.find('.ice').css(top: typeEl.position().top + typeEl.width())
     content = @$el.find(".text").text()
     @$el.find(".text").html(@iconify(content))


Fixes #41

Fixes #41

--- src/controller.main.coffee
+++ src/controller.main.coffee
@@ -169,12 +169,13 @@
       c.log.debug val
     addFolder: ->
       c.log.debug "AddFolder called"
-      dialog.showOpenDialog {properties: ['openDirectory', 'createDirectory']}, (files) ->
+      dialog.showOpenDialog {properties: ['openDirectory', 'createDirectory']}, (files) =>
         return if files is undefined
         f = files[0]
         dfc.addDataflux f
+        @folders = dfc.getObjects()
         dfc.write()
-        #@folders = dfc.getObjects()
+
     removeFolder: (index) ->
       if index is @activeIndex
         @activeIndex = 0


Make a new -- and **different** -- request only when **really** necessary

--- app/scripts/controllers/last-requests.coffee
+++ app/scripts/controllers/last-requests.coffee
@@ -69,11 +69,13 @@
     @getLastRequests(@currentPage, pageSize)
     @getRequestsInLastDay()

-  onDomainFilterChange: =>
-    @onPageChange()
+  onDomainFilterChange: (newVal, oldVal) =>
+    if newVal != oldVal
+      @onPageChange()

-  onStatusCodeFilterChange: =>
-    @onPageChange()
+  onStatusCodeFilterChange: (newVal, oldVal) =>
+    if newVal != oldVal
+      @onPageChange()

   watchScope: ->
     @scope.$watch('model.domainFilter', @onDomainFilterChange)


constantly clear out the error markers

constantly clear out the error markers

--- library/semantic/controls.coffee
+++ library/semantic/controls.coffee
@@ -66,7 +66,7 @@
         assert _.isObject input = $ "<input>", type: "password"
         field = $("<div>", class: "field").appendTo @container
         wrap = $ "<div>", class: "icon input ui left labeled"
-        input.one "input", -> do -> field.removeClass "error"
+        input.on "input", -> do -> field.removeClass "error"
         assert corner = $ "<div>", class: "ui corner label"
         assert asterisk = $ "<i>", class: "icon asterisk"
         assert icon = $ "<i>", class: "icon #{icon}" if icon
@@ -86,7 +86,7 @@
         assert _.isObject label = $ "<label>", class: "label"
         field = $("<div>", class: "field").appendTo @container
         wrap = $ "<div>", class: "icon input ui left labeled"
-        input.one "input", -> do -> field.removeClass "error"
+        input.on "input", -> do -> field.removeClass "error"
         assert corner = $ "<div>", class: "ui corner label"
         assert asterisk = $ "<i>", class: "icon asterisk"
         assert icon = $ "<i>", class: "icon #{icon}" if icon
@@ -107,7 +107,7 @@
         assert _.isObject input = $ "<input>", type: "checkbox"
         field = $("<div>", class: "field").appendTo @container
         wrap = $ "<div>", class: what; field.addClass "inline"
-        input.one "input", -> do -> field.removeClass "error"
+        input.on "input", -> do -> field.removeClass "error"
         try $(field).data "identity", identity.toString()
         field.addClass try identity.toString() if identity
         input.appendTo wrap; field.append wrap.append label
@@ -124,7 +124,7 @@
         assert _.isObject label = $ "<label>", class: "label"
         field = $("<div>", class: "field").appendTo @container
         wrap = $ "<div>", class: "icon input ui left labeled"
-        input.one "input", -> do -> field.removeClass "error"
+        input.on "input", -> do -> field.removeClass "error"
         assert icon = $ "<i>", class: "icon #{icon}" if icon
         assert input.attr placeholder: synopsis.toString()
         try $(field).data "identity", identity.toString()


Fix method name mistake

Fix method name mistake

--- library/visualize/monitor.coffee
+++ library/visualize/monitor.coffee
@@ -76,7 +76,7 @@
     # element instance. This method will attach the specific event
     # handlers for all of the reference events, which will transfer
     # the event into the corresponding event on the bound transport.
-    attachToTransport: -> for descriptor in @constructor.REFERENCE
+    attachToElement: -> for descriptor in @constructor.REFERENCE
         @element.on descriptor.event, (event, origin, context) ->
             tagging = @element.tag or undefined
             missing = "The element has not tag attached"


more readily invalidate highlights

more readily invalidate highlights

--- lib/editor/highlights.coffee
+++ lib/editor/highlights.coffee
@@ -13,7 +13,8 @@
   errorLines: (ls) ->
     markers = []
     watch = @observeLines ls, (ed, {line}) =>
-      m = ed.markBufferRange [[line, 0], [line+1, 0]]
+      m = ed.markBufferRange [[line, 0], [line+1, 0]],
+        invalidate: 'touch'
       markers.push m
       ed.decorateMarker m,
         type: 'highlight'


minor comments

minor comments

--- fizzylogo_UserDefinedClass.coffee
+++ fizzylogo_UserDefinedClass.coffee
@@ -9,7 +9,8 @@
   #   FLBoolean = new FLBooleanPrimitiveClass()
   #
   # the .flClass of this object will point to the object
-  # itself, just like in all fizzylogo classes.
+  # itself, just like in all fizzylogo classes, since
+  # the class of each class is itself.
   constructor: ->
     super()
     @value = "some_custom_class_of_user"


change class id

change class id

--- library/semantic/abstract.coffee
+++ library/semantic/abstract.coffee
@@ -85,7 +85,7 @@
         assert @reference = reference # store reference
         assert _.isFunction(@constructor::element), ptf
         @element = $ renderable(@constructor::element) @
-        assert @element.addClass "gf-semantic-widget"
+        assert @element.addClass "semantic-ui-widget"
         assert @element.appendTo this.container or 0
         payload.call this, @element, @reference or 0
         assert identify = try @constructor.identify()


Only grab status when we're asked for the view.

--- lib/fuzzy-finder-view.coffee
+++ lib/fuzzy-finder-view.coffee
@@ -64,8 +64,7 @@
     @subscriptions?.dispose()
     @subscriptions = null

-  viewForItem: ({filePath, projectRelativePath, status}) ->
-
+  viewForItem: ({filePath, projectRelativePath}) ->
     # Style matched characters in search results
     filterQuery = @getFilterQuery()

@@ -98,11 +97,15 @@

       @li class: 'two-lines', =>
-        if (repo = repositoryForPath(filePath))? and status?
-          if repo.isStatusNew(status)
-            @div class: 'status status-added icon icon-diff-added'
-          else if repo.isStatusModified(status)
-            @div class: 'status status-modified icon icon-diff-modified'
+        if (repo = repositoryForPath(filePath))?
+          id = encodeURIComponent("fuzzy-finder-#{filePath}")
+          @div class: 'status', id: id
+          repo.getCachedPathStatus(filePath).then (status) ->
+            statusNode = $(document.getElementById(id))
+            if statusNode? and repo.isStatusNew(status)
+              statusNode.addClass('status-added icon icon-diff-added')
+            else if statusNode? and repo.isStatusModified(status)
+              statusNode.addClass('status-modified icon icon-diff-modified')

         ext = path.extname(filePath)
         if fs.isReadmePath(filePath)


add a default success handler

add a default success handler

--- library/semantic/platings.coffee
+++ library/semantic/platings.coffee
@@ -83,3 +83,20 @@
         @forms = new Shipped @content, "sign-up-form"
         @emit "populate-form", disabler, enabler
         @on "negative", => try closer().click()
+
+    # This method is invoked once the `success` event goes off in
+    # the sercvice. This event could be fired after the component
+    # receives a response after the form submission, if it was a
+    # success. This implementation hides the actualy form, shows
+    # the generic acknowledgement memo regarding succeeded form.
+    respondedWithSuccess: @awaiting "success", ->
+        this.paragraph.text "The information you entered in
+        to the form has been successfully submitted. Please
+        press the okay button to close this window and carry
+        on whatever you were doing prior to filling a form."
+        acknowledge = $ "<div>", class: "ui positive button"
+        acknowledge.append $ "<i>", class: "checkmark icon"
+        assert acknowledge.addClass "right labeled icon"
+        $(acknowledge).click => this.window.modal "hide"
+        acknowledge.prepend $("<span>").text "okay"
+        acknowledge.appendTo @actions; return this


Use stub instead of spy in buttonview

Use stub instead of spy in buttonview

--- test/components/buttons/buttonview.coffee
+++ test/components/buttons/buttonview.coffee
@@ -7,7 +7,7 @@
 describe 'KDButtonView', ->
   beforeEach ->
     @sinon = sinon.sandbox.create()
-    @callback = @sinon.spy (->)
+    @callback = @sinon.stub()
     @view = new KDButtonView
       title: 'button'
       iconClass: 'cupid-green'


Fixed submitForm method

Fixed submitForm method

--- clickingmonkey.coffee
+++ clickingmonkey.coffee
@@ -22,6 +22,6 @@
       delay 1
     @

-  @submitForm = (delay=1000) =>
-    @clickElem '[type=submit]', delay
+  @submitForm = (ms=1000) =>
+    @clickElem '[type=submit]', ms
   @


Fix for monitor timeout not getting cleared when durable channel ends

--- src/durable-channel.coffee
+++ src/durable-channel.coffee
@@ -8,7 +8,9 @@

   constructor: (options) ->
     super
-
+
+    @ending = false
+
     {@name, @timeoutMonitorFrequency, @transport} = options

     unless @name?
@@ -137,7 +139,8 @@
                 events.emit("success") if returned == expiredMessages.length
               _events.on "error", (err) -> events.emit "error", err
         go =>
-          @timeoutMonitor = setTimeout(loopToMonitor, @timeoutMonitorFrequency)
+          if !@ending
+            @timeoutMonitor = setTimeout(loopToMonitor, @timeoutMonitorFrequency)

     @timeoutMonitor = setTimeout(loopToMonitor, @timeoutMonitorFrequency)

@@ -233,6 +236,7 @@
         events.emit "success"

   end: ->
+    @ending = true
     clearTimeout @timeoutMonitor
     @transport.end("#{@name}.queue", !@isTransportShared)


Kill all received tokens when we already have one

--- src/public/library/node.structured.coffee
+++ src/public/library/node.structured.coffee
@@ -167,10 +167,12 @@
 			if @isSuperNode
 				callback?(false)
 				return
+
 			console.log 'sending parent request to ' + peer.id
-			peer.query('peer.requestAdoption', @id, (accepted) =>
-				unless accepted?
+			peer.query('peer.requestAdoption', @id, ( accepted ) =>
+				unless accepted
 					callback?(false)
+
 				if accepted and not @isSuperNode
 					console.log 'parent request accepted'
 					if @_parent?
@@ -302,7 +304,6 @@
 			if @isSuperNode is superNode
 				return

-
 			console.log "Supernode: #{superNode} and having token", @token?
 			@isSuperNode = superNode

@@ -369,8 +370,9 @@
 		_onPeerSetSuperNode: ( id, superNode ) =>
 			unless peer = @getPeer(id)
 				if superNode and @isSuperNode
-					peer = @connect(id, () =>
-						@addSibling(peer)
+					peer = @connect(id, ( success ) =>
+						if success
+							@addSibling(peer)
 					)
 			else peer.isSuperNode = superNode

@@ -407,8 +409,11 @@

 				# Else, first connect, then set as parent.
 				else
-					peer = @connect(superNode.id, ( ) =>
-						console.log peer
+					peer = @connect(superNode.id, ( success ) =>
+						unless success
+							connectParent(superNodes)
+							return
+
 						@setParent(peer, ( accepted ) =>
 							if accepted
 								@trigger('joined')
@@ -476,8 +481,9 @@
 							if peer = @getPeer(superNode.id)
 								@addSibling(peer)
 							else
-								peer = @connect(superNode.id, ( ) =>
-									@addSibling(peer)
+								peer = @connect(superNode.id, ( success ) =>
+									if success
+										@addSibling(peer)
 								)
 						) (superNode)
 				)
@@ -581,8 +587,9 @@
 			if peer = @getPeer(id)
 				@setParent(peer)
 			else
-				peer = @connect(id, ( ) =>
-					@setParent(peer)
+				peer = @connect(id, ( success ) =>
+					if success
+						@setParent(peer)
 				)

 		# Adds a token to the collection of foreign tokens
@@ -620,6 +627,7 @@
 		#
 		_onTokenReceived: ( tokenString, timestamp, message ) =>
 			if @token?
+				@broadcast('token.die', tokenString)
 				return

 			@token = Token.deserialize(tokenString)


Add some more assertions to the wrapAccessor tests to ensure that passing multiple keys for the accessor object works.

--- tests/batman/object/class_dsl_test.coffee
+++ tests/batman/object/class_dsl_test.coffee
@@ -46,27 +46,29 @@
   class PlusOne extends Batman.Object
     @wrapAccessor (core) ->
       get: (key) -> core.get.apply(this, arguments) + 1
-
+      set: (key, value) -> core.set.call(@, key, value + 1)
+
   example = new PlusOne
   example.set('foo', 1)
-  equal example.foo, 1
-  equal example.get('foo'), 2
+  equal example.foo, 2
+  equal example.get('foo'), 3

 test "@wrapAccessor can be passed an accessor object directly instead of an accessor-returning function", ->
   class PlusOne extends Batman.Object
     @wrapAccessor
+      get: (key) -> @[key] + 1
       set: (key, val) -> @[key] = val + 1
-
+
   example = new PlusOne
   example.set('foo', 1)
   equal example.foo, 2
-  equal example.get('foo'), 2
+  equal example.get('foo'), 3

 test "@wrapAccessor can be given a wrapper function for specific keys", ->
   class PlusOne extends Batman.Object
     @wrapAccessor 'foo', 'bar', (core) ->
       set: (key, val) -> core.set.call(this, key, val+1)
-
+
   example = new PlusOne
   example.set('foo', 1)
   equal example.foo, 2
@@ -82,7 +84,7 @@
   class PlusOne extends Batman.Object
     @wrapAccessor 'foo', 'bar'
       get: (key) -> @[key] + 1
-
+
   example = new PlusOne
   example.set('foo', 1)
   equal example.foo, 1


engine_selector

--- src/engine_selector.coffee
+++ src/engine_selector.coffee
@@ -23,13 +23,19 @@
      player = engine.add
        class: "Player"

-     enemy = enemy.add
+     enemy = engine.add
        class: "Enemy"
        speed: 5
+       x: 0

-     boss = enemy.add
+     distantEnemy = engine.add
+       class "Enemy"
+       x: 500
+
+     boss = engine.add
        class: "Enemy"
        id: "Boss"
+       x: 0

      # to select an object by id use "#anId"
      engine.find "#Boss"
@@ -42,13 +48,9 @@
      # to select an object by properties use ".someProperty" or ".someProperty=someValue"
      engine.find ".speed=5"
   => [enemy]
+
+     # You may mix and match selectors. "Enemy
   </pre></code>
-
-  To select an object by id use "#anId"
-
-  To select objects by class use "MyClass"
-
-  To select objects by properties use ".someProperty" or ".someProperty=someValue"

   You may mix and match selectors. "Wall.x=0" to select all objects of class Wall with an x property of 0.


Remove search logging

Remove search logging

--- lib/project-find-view.coffee
+++ lib/project-find-view.coffee
@@ -150,9 +150,7 @@
     @findHistory.store()

     deferred = @search()
-    console.time("search")
     deferred.done =>
-      console.timeEnd("search")
       @loadingMessage.hide()
       @previewCount.text(@getResultCountText())


Extracting to function

Extracting to function

--- src/script/portal-minicart.coffee
+++ src/script/portal-minicart.coffee
@@ -29,22 +29,28 @@
 	startHelpers: =>
 		dust.helpers.formatDate = (chunk, context, bodies, params) ->
 			timestamp = params.date
-			date = new Date(timestamp)
-			day = date.getDate()
-			month = date.getMonth() + 1
-			fullYear = date.getFullYear()
-			twoDigitsDay = ("0" + day).slice(-2)
-			twoDigitsMonth = ("0" + month).slice(-2)
-			return chunk.write("#{twoDigitsDay}/#{twoDigitsMonth}/#{fullYear}")
+			return chunk.write(formatDate(timestamp))

 		dust.helpers.formatMoment = (chunk, context, bodies, params) ->
 			timestamp = params.date
-			date = new Date(timestamp)
-			hour = date.getHours()
-			minutes = date.getMinutes()
-			twoDigitsHour = ("0" + hour).slice(-2)
-			twoDigitsMinutes = ("0" + minutes).slice(-2)
-			return chunk.write("#{twoDigitsHour}:#{twoDigitsMinutes}")
+			return chunk.write(formatMoment(timestamp))
+
+	formatMoment = (timestamp) =>
+		date = new Date(timestamp)
+		hour = date.getHours()
+		minutes = date.getMinutes()
+		twoDigitsHour = ("0" + hour).slice(-2)
+		twoDigitsMinutes = ("0" + minutes).slice(-2)
+		return "#{twoDigitsHour}:#{twoDigitsMinutes}"
+
+	formatDate = (timestamp) =>
+		date = new Date(timestamp)
+		day = date.getDate()
+		month = date.getMonth() + 1
+		fullYear = date.getFullYear()
+		twoDigitsDay = ("0" + day).slice(-2)
+		twoDigitsMonth = ("0" + month).slice(-2)
+		return "#{twoDigitsDay}/#{twoDigitsMonth}/#{fullYear}"

 	getOrderFormURL: =>
 		@options.orderFormURL


listen to remodeling

listen to remodeling

--- views/components/ship/index.cjsx
+++ views/components/ship/index.cjsx
@@ -98,7 +98,7 @@
           inBattle = [false, false, false, false]
           goback = {}
           combined = body.api_combined_flag? && body.api_combined_flag > 0
-        when '/kcsapi/api_req_hensei/change', '/kcsapi/api_req_hokyu/charge', '/kcsapi/api_get_member/deck', '/kcsapi/api_get_member/ship_deck', '/kcsapi/api_get_member/ship2', '/kcsapi/api_get_member/ship3', '/kcsapi/api_req_kousyou/destroyship', '/kcsapi/api_req_kaisou/powerup', '/kcsapi/api_req_nyukyo/start', '/kcsapi/api_req_nyukyo/speedchange', '/kcsapi/api_req_hensei/preset_select', '/kcsapi/api_req_kaisou/slot_exchange_index'
+        when '/kcsapi/api_req_hensei/change', '/kcsapi/api_req_hokyu/charge', '/kcsapi/api_get_member/deck', '/kcsapi/api_get_member/ship_deck', '/kcsapi/api_get_member/ship2', '/kcsapi/api_get_member/ship3', '/kcsapi/api_req_kousyou/destroyship', '/kcsapi/api_req_kaisou/powerup', '/kcsapi/api_req_nyukyo/start', '/kcsapi/api_req_nyukyo/speedchange', '/kcsapi/api_req_hensei/preset_select', '/kcsapi/api_req_kaisou/slot_exchange_index', '/kcsapi/api_req_kaisou/remodeling'
           true
         when '/kcsapi/api_req_sortie/battleresult', '/kcsapi/api_req_combined_battle/battleresult'
           {decks} = @state


remember token on login

remember token on login

--- src/shared/component/login.coffee
+++ src/shared/component/login.coffee
@@ -9,6 +9,7 @@
   validateLogin
   classNames
   urlUsers
+  rememberToken
 ) ->
   React.createClass
     mixins: [Cursors]
@@ -52,14 +53,14 @@
       promise = login(that.state.page.data)
       promise
         .then (data) ->
+          rememberToken data.token
           that.update
             # navigate
             path: {$set: urlUsers.stringify()}
-            token: {$set: data.token}
             currentUser: {$set: data.user}
             page:
               alert: {$set: null}
-        .fail (error) ->
+        .catch (error) ->
           # TODO extract this
           if error.status is 422
             that.update {page: {alert: {$set: error.response}}}
@@ -92,7 +93,6 @@
           k.build ComponentNavigation,
             cursors:
               currentUser: that.getCursor('currentUser')
-              token: that.getCursor('token')
               path: that.getCursor('path')
           k.div {className: 'container'}, ->
             k.div {className: 'row'}, ->


fix error in logging, which might have caused further problems (?)

--- src/smc-hub/compute-client.coffee
+++ src/smc-hub/compute-client.coffee
@@ -1855,13 +1855,13 @@
     address: (opts) =>
         opts = defaults opts,
             cb : required
+        dbg = @dbg("address")
         if @_address_cbs?
             @_address_cbs.push(opts.cb)
             dbg("address already running, so adding callback to queue; it now has length #{@_address_cbs.length}")
             return
         @_synctable?.connect()
         @_address_cbs = [opts.cb]
-        dbg = @dbg("address")
         dbg()
         address = undefined
         misc.retry_until_success


webapp/course: cleaning up and fixing the logic when to show the return button

--- src/smc-webapp/course/common.cjsx
+++ src/smc-webapp/course/common.cjsx
@@ -287,8 +287,12 @@
     render: ->
         peer_grade = @props.assignment.get('peer_grade')?.get('enabled')
         skip_grading = @props.assignment.get('skip_grading') ? false
-        show_grade_col = (!skip_grading) and ((peer_grade and @props.info.last_peer_collect) or (not peer_grade and @props.info.last_collect))
-        show_return_graded = @props.grade or (skip_grading and @props.info.last_collect)
+        if peer_grade
+            show_grade_col = !skip_grading and @props.info.last_peer_collect
+            show_return_graded = @props.grade or (skip_grading and @props.info.last_peer_collect)
+        else
+            show_grade_col = !skip_grading and @props.info.last_collect
+            show_return_graded = @props.grade or (skip_grading and @props.info.last_collect)
         width = if peer_grade then 2 else 3
         <Row style={borderTop:'1px solid #aaa', paddingTop:'5px', paddingBottom: '5px'}>
             <Col md=2 key="title">


implement https://github.com/sagemathinc/cocalc/issues/2655 -- include student id in course export

--- src/smc-webapp/course/settings_panel.cjsx
+++ src/smc-webapp/course/settings_panel.cjsx
@@ -236,9 +236,11 @@
         </h4>

     path: (ext) ->
-        p = @props.path
+        # make path more likely to be python-readable...
+        p = misc.replace_all(@props.path, '-', '_')
+        p = misc.split(p).join('_')
         i = p.lastIndexOf('.')
-        return p.slice(0,i) + '.' + ext
+        return 'export_' + p.slice(0,i) + '.' + ext

     open_file: (path) ->
         @actions(project_id : @props.project_id).open_file(path:path,foreground:true)
@@ -266,7 +268,7 @@
         timestamp  = (webapp_client.server_time()).toISOString()
         content = "# Course '#{@props.settings.get('title')}'\n"
         content += "# exported #{timestamp}\n"
-        content += "Name,Email,"
+        content += "Name,Id,Email,"
         content += ("\"grade: #{assignment.get('path')}\"" for assignment in assignments).join(',') + ','
         content += ("\"comments: #{assignment.get('path')}\"" for assignment in assignments).join(',') + '\n'
         for student in store.get_sorted_students()
@@ -276,7 +278,8 @@
             comments = comments.replace(/\n/g, "\\n")
             name     = "\"#{store.get_student_name(student)}\""
             email    = "\"#{store.get_student_email(student) ? ''}\""
-            line     = [name, email, grades, comments].join(',')
+            id       = "\"#{student.get('student_id')}\""
+            line     = [name, id, email, grades, comments].join(',')
             content += line + '\n'
         @write_file(@path('csv'), content)

@@ -310,7 +313,8 @@
             name     = store.get_student_name(student)
             email    = store.get_student_email(student)
             email    = if email? then "'#{email}'" else 'None'
-            line     = "    {'name':'#{name}', 'email':#{email}, 'grades':[#{grades}], 'comments':[#{comments}]},"
+            id       = student.get('student_id')
+            line     = "    {'name':'#{name}', 'id':'#{id}', 'email':#{email}, 'grades':[#{grades}], 'comments':[#{comments}]},"
             content += line + '\n'
         content += ']\n'
         @write_file(@path('py'), content)


Started rebuilding the CSSTransitioner to support separate activate/deactivate animation classes

--- src/spine/modules/css_transitioner.coffee
+++ src/spine/modules/css_transitioner.coffee
@@ -1,24 +1,32 @@
 CSSTransitioner =
   cssTransitionEndEvents: 'transitionend webkitTransitionEnd oTransitionEnd otransitionend MSTransitionEnd'
-  cssActiveClass: 'active'
+  cssActivateClass: 'active'
+  cssDeactivateClass: undefined
   cssTransitionDelay: 10

   doActivate: ->
-    @cssStartTransition('addClass', @onActivated)
+    if @cssDeactivateClass
+      @el.removeClass @cssDeactivateClass
+
+    @cssStartTransition('addClass', @cssActivateClass, @onActivated)

   doDeactivate: ->
-    @cssStartTransition('removeClass', @onDeactivated)
+    if @cssDeactivateClass
+      @cssStartTransition('addClass', @cssDeactivateClass, @onDeactivated)
+    else
+      @cssStartTransition('removeClass', @cssActivateClass, @onDeactivated)

-  cssListen: (callback)->
-    @el.on @cssTransitionEndEvents, =>
+  cssListen: (callback, className)->
+    $(@el).bind @cssTransitionEndEvents, =>
       callback.call @
       @el.off @cssTransitionEndEvents
+      #@el.removeClass className

-  cssStartTransition: (mutatorFunc, callback)->
-    @cssListen(callback)
+  cssStartTransition: (mutatorFunc, className, callback)->
+    @cssListen(callback, className)

     @delay =>
-      @el[mutatorFunc].call @el, @cssActiveClass
+      @el[mutatorFunc].call @el, className
     , @cssTransitionDelay

 Exo?.Spine ||= {}


use ErrorComponent directly

use ErrorComponent directly

--- app/builtins.coffee
+++ app/builtins.coffee
@@ -146,15 +146,17 @@

   ErrorComponent = React.createClass
     displayName: 'ErrorComponent'
-    render: -> React.DOM.pre {className: 'error'}, @props.message
+    render: ->
+      message = @props.message
+      if not message?
+        message = 'Unknown error'
+        # TODO include stack trace?
+      else if not _.isString message
+        message = message.toString()
+        # TODO handle exceptions better
+      React.DOM.pre {className: 'error'}, message

   component_fn 'error', 'Shows a preformatted error message', (ctx, message) ->
-    if not message?
-      message = 'Unknown error'
-      # TODO include stack trace?
-    else if not _.isString message
-      message = message.toString()
-      # TODO handle exceptions better
     ErrorComponent {message}

   component_fn 'example', 'Makes a clickable code example', (ctx, value, opts) ->


added subreddit

added subreddit

--- web/src/cs/sources/reddit.coffee
+++ web/src/cs/sources/reddit.coffee
@@ -10,7 +10,8 @@
 	'gifs',
 	'perfectloops',
 	'woahdude',
-	'earthporn'
+	'earthporn',
+	'gunners'
 ]
 options = {
 	subreddit: subs[0]