Remove done/wontfix TODOS + add comments

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -17,6 +17,8 @@
         # Constants
         limit: 40 # How many results we display per page // Final for now
         line_height: 13 # Define the height of a line (used for a line is too long)
+        size_history: 100
+
         events:
             'mouseup .CodeMirror': 'handle_click'
             'mousedown .suggestion_name_li': 'select_suggestion' # Keep mousedown to compete with blur on .input_query
@@ -49,6 +51,7 @@
         descriptions: {}
         suggestions: {} # Suggestions[state] = function for this state

+        # Once we are done moving the doc, we could generate a .js in the makefile file with the data so we don't have to do an ajax request+all this stuff
         set_doc_description: (command, tag, suggestions) =>
             if tag is 'run' # run is a special case, we don't want use to pass a callback, so we change a little the body
                 full_tag = tag+'(' # full tag is the name plus a parenthesis (we will match the parenthesis too)
@@ -141,7 +144,10 @@
                 # In case we give focus to codemirror then load the docs, we show the suggestion
                 window.router.current_view.handle_keypress()

+        # Save the query in the history
+        # The size of the history is infinite per session. But we will just save @size_history queries in localStorage
         save_query: (query) =>
+            # Remove empty lines
             query = query.replace(/^\s*$[\n\r]{1,}/gm, '')
             if query[query.length-1] is '\n' or query[query.length-1] is '\r'
                 query = query.slice 0, query.length-1
@@ -153,11 +159,13 @@
                     else
                         window.localStorage.rethinkdb_history = JSON.stringify @history
                     @history_view.add_query query
+
         clear_history: =>
             @history = []
             window.localStorage.rethinkdb_history = JSON.stringify @history

         initialize: (args) =>
+            # We do not load data from localStorage.
             if not DataExplorerView.Container.prototype.saved_data?
                 DataExplorerView.Container.prototype.saved_data =
                     current_query: null
@@ -169,7 +177,6 @@
                     view: 'tree'

             # Load history, keep it in memory for the session
-            # The size of the history is infinite per session. But each session will not load with more that @size_history entries
             if not DataExplorerView.Container.prototype.history?
                 if window.localStorage?.rethinkdb_history?
                     try
@@ -186,6 +193,7 @@
             @show_query_warning = @saved_data.query isnt @saved_data.current_query
             @current_results = @saved_data.results

+            # Index used to navigate through history with the keyboard
             @history_displayed_id = 0

             # We escape the last function because we are building a regex on top of it.
@@ -235,7 +243,6 @@
                 limit: @limit
                 view: @saved_data.view

-
             @history_view = new DataExplorerView.HistoryView
                 container: @
                 history: @history
@@ -245,6 +252,7 @@
                 on_success: @success_on_connect
                 on_fail: @error_on_connect

+            # One callback to rule them all
             $(window).mousemove @handle_mousemove
             $(window).mouseup @handle_mouseup
             @render()
@@ -297,8 +305,7 @@

             return @

-        # Create a code mirror instance
-        # This method has to be called AFTER the el element has been inserted in the DOM tree.
+        # This method has to be called AFTER the el element has been inserted in the DOM tree, mostly for codemirror
         init_after_dom_rendered: =>
             @codemirror = CodeMirror.fromTextArea document.getElementById('input_query'),
                 mode:
@@ -315,7 +322,11 @@
             if @saved_data.current_query?
                 @codemirror.setValue @saved_data.current_query
             @codemirror.focus() # Give focus
+
+            # Track if the focus is on codemirror
+            # We use it to refresh the docs once the reql_docs.json is loaded
             @prototype.focus_on_codemirror = true
+
             @codemirror.setCursor @codemirror.lineCount(), 0
             if @codemirror.getValue() is '' # We show suggestion for an empty query only
                 @handle_keypress()
@@ -352,10 +363,11 @@
         handle_click: (event) =>
             @handle_keypress null, event

-        # Core of the suggestions' system: We have to parse the query
+        # Handle events on codemirror
         # Return true if we want code mirror to ignore the event
         handle_keypress: (editor, event) =>
             @prototype.focus_on_codemirror = true
+
             # Let's hide the tooltip if the user just clicked on the textarea. We'll only display later the suggestions if there are (no description)
             if event?.type is 'mouseup'
                 @hide_suggestion_and_description()
@@ -363,6 +375,7 @@
             # Save the last query (even incomplete)
             @saved_data.current_query = @codemirror.getValue()

+            # Look for special commands
             if event?.which?
                 if event.which is 27 # ESC
                     @hide_suggestion_and_description()
@@ -388,27 +401,31 @@
                                 suggestion_to_write: @current_suggestions[@current_highlighted_suggestion] # Auto complete with the highlighted suggestion
                             @ignore_tab_keyup = true # If we are switching suggestion, we don't want to do anything else related to tab
                             return true
-                else if event.which is 13 and (event.shiftKey or event.ctrlKey or event.metaKey) # If the user hit enter and (Ctrl or Shift)
+                # If the user hit enter and (Ctrl or Shift)
+                else if event.which is 13 and (event.shiftKey or event.ctrlKey or event.metaKey)
                     @hide_suggestion_and_description()
                     event.preventDefault()
                     if event.type isnt 'keydown'
                         return true
                     @execute_query()
                     return true
-                else if (event.ctrlKey or event.metaKey) and event.which is 86 and event.type is 'keydown' # Ctrl + V
+                # Ctrl/Cmd + V
+                else if (event.ctrlKey or event.metaKey) and event.which is 86 and event.type is 'keydown'
                     @last_action_is_paste = true
                     @num_released_keys = 0 # We want to know when the user release Ctrl AND V
                     if event.metaKey
                         @num_released_keys++ # Because on OS X, the keyup event is not fired when the metaKey is pressed (true for Firefox, Chrome, Safari at least...)
                     @hide_suggestion_and_description()
                     return true
-                else if event.type is 'keyup' and @last_action_is_paste is true and (event.which is 17 or event.which is 91) # When the user release Ctrl after a ctrl + V
+                # When the user release Ctrl/Cmd after a Ctrl/Cmd + V
+                else if event.type is 'keyup' and @last_action_is_paste is true and (event.which is 17 or event.which is 91)
                     @num_released_keys++
                     if @num_released_keys is 2
                         @last_action_is_paste = false
                     @hide_suggestion_and_description()
                     return true
-                else if event.type is 'keyup' and @last_action_is_paste is true and event.which is 86 # When the user release V after a ctrl + V
+                # When the user release V after a Ctrl/Cmd + V
+                else if event.type is 'keyup' and @last_action_is_paste is true and event.which is 86
                     @num_released_keys++
                     if @num_released_keys is 2
                         @last_action_is_paste = false
@@ -451,31 +468,34 @@
                     if event.type isnt 'keydown'
                         return true
                     @execute_query()
-                    return true
+                    return true # We do not replace the selection with a new line
                 # If the user select something and end somehwere with suggestion
                 if event?.type isnt 'mouseup'
                     return false
                 else
                     return true

-            if @$('.suggestion_name_li_hl').length > 0 # If there is a hilighted suggestion, we want to catch enter
+            # If there is a hilighted suggestion, we want to catch enter
+            if @$('.suggestion_name_li_hl').length > 0
                 if event?.which is 13
                     event.preventDefault()
                     @handle_keypress()
                     return true

-            if @history_displayed_id isnt 0 and event? # We are scrolling in history
+            # We are scrolling in history
+            if @history_displayed_id isnt 0 and event?
                 # We catch ctrl, shift, alt,
                 if event.ctrlKey or event.shiftKey or event.altKey or event.which is 16 or event.which is 17 or event.which is 18 or event.which is 20 or event.which is 91 or event.which is 92 or event.type of @mouse_type_event
                     return false
+
             # Avoid arrows+home+end+page down+pageup
             # if event? and (event.which is 24 or event.which is ..)
-            # 0 is for firefox
+            # 0 is for firefox...
             if not event? or (event.which isnt 37 and event.which isnt 38 and event.which isnt 39 and event.which isnt 40 and event.which isnt 33 and event.which isnt 34 and event.which isnt 35 and event.which isnt 36 and event.which isnt 0)
                 @history_displayed_id = 0
                 @draft = @codemirror.getValue()

-            if event?.which isnt 9 # has to be before create_suggestion
+            if event?.which isnt 9 # has to be before create_suggestion()
                 @cursor_for_auto_completion = @codemirror.getCursor()

             # We just look at key up so we don't fire the call 3 times
@@ -483,7 +503,8 @@
                 return false
             if event?.which is 16 # We don't do anything with Shift.
                 return false
-            # Tab is an exception, we let it pass (we tab bring back suggestions) - What we want is to catch keydown
+
+            # Tab is an exception, we let it pass (tab bring back suggestions) - What we want is to catch keydown
             if @ignore_tab_keyup is true and event?.which is 9
                 if event.type is 'keyup'
                     @ignore_tab_keyup = false
@@ -513,16 +534,17 @@
                         query_after_cursor += query_lines[i]
             @query_last_part = query_after_cursor

-            # We could perform better, but that would make the code harder to read, so let's not work too hard
             stack = @extract_data_from_query
                 query: query_before_cursor
                 position: 0

             result =
                 status: null
+                # create_suggestion is going to fill to_complete and to_describe
                 #to_complete: undefined
                 #to_describe: undefined

+            # If we are in the middle of a function (text after the cursor - that is not an element in @char_breakers), we just show a description, not a suggestion
             result_non_white_char_after_cursor = @regex.get_first_non_white_char.exec(query_after_cursor)
             if result_non_white_char_after_cursor isnt null and not(result_non_white_char_after_cursor[1]?[0] of @char_breakers)
                 result.status = 'break_and_look_for_description'
@@ -533,6 +555,7 @@
                     result.status = 'break_and_look_for_description'
                     @hide_suggestion()

+            # Create the suggestion/description
             @create_suggestion
                 stack: stack
                 query: query_before_cursor
@@ -586,7 +609,7 @@
             multiple_line_comment: /^(\s)*\/\*[^]*\*\//
             get_first_non_white_char: /\s*(\S+)/
             last_char_is_white: /.*(\s+)$/
-        stop_char: # Just for performance
+        stop_char: # Just for performance and cleaner code
             opening:
                 '(': true
                 '{': true
@@ -597,17 +620,14 @@
                 ']': '['

-        ###
-        element.type in ['string', 'function', 'var', 'separator', 'anonymous_function', 'object']
-        TODO Collapse the query.slice(...)
-        ###
+        # We build a stack of the query.
+        # Chained functions are in the same array, arguments/inner queries are in a nested array
+        # element.type in ['string', 'function', 'var', 'separator', 'anonymous_function', 'object', 'array_entry', 'object_key' 'array']
         extract_data_from_query: (args) =>
             query = args.query
             context = if args.context? then DataUtils.deep_copy(args.context) else {}
             position = args.position

-            # query_after_cursor = args.query_before_cursor
-
             stack = []
             element =
                 type: null
@@ -663,7 +683,8 @@
                                 start += result_white[0].length
                                 continue

-                            result_regex = @regex.anonymous.exec query.slice i # Check for anonymouse function
+                            # Check for anonymouse function
+                            result_regex = @regex.anonymous.exec query.slice i
                             if result_regex isnt null
                                 element.type = 'anonymous_function'
                                 list_args = result_regex[2]?.split(',')
@@ -677,8 +698,9 @@
                                 body_start = i+result_regex[0].length
                                 stack_stop_char = ['{']
                                 continue
-
-                            result_regex = @regex.return.exec query.slice i # Check for return
+
+                            # Check for return
+                            result_regex = @regex.return.exec query.slice i
                             if result_regex isnt null
                                 # I'm not sure we need to keep track of return, but let's keep it for now
                                 element.type = 'return'
@@ -693,7 +715,8 @@
                                 start = i+result_regex[0].length
                                 continue

-                            result_regex = @regex.object.exec query.slice i # Check for object
+                            # Check for object
+                            result_regex = @regex.object.exec query.slice i
                             if result_regex isnt null
                                 element.type = 'object'
                                 element.next_key = null
@@ -703,7 +726,8 @@
                                 stack_stop_char = ['{']
                                 continue

-                            result_regex = @regex.array.exec query.slice i # Check for array
+                            # Check for array
+                            result_regex = @regex.array.exec query.slice i
                             if result_regex isnt null
                                 element.type = 'array'
                                 element.next_key = null
@@ -718,9 +742,9 @@
                             else
                                 new_start = i

-                            result_regex = @regex.method.exec query.slice new_start # Check for a standard method
+                            # Check for a standard method
+                            result_regex = @regex.method.exec query.slice new_start
                             if result_regex isnt null
-                                # We could check if we are matching row( and split it in "row" and "("
                                 if stack[stack.length-1]?.type is 'function' or stack[stack.length-1]?.type is 'var' # We want the query to start with r. or arg.
                                     element.type = 'function'
                                     element.name = result_regex[0]
@@ -733,7 +757,6 @@
                                     position_opening_parenthesis = result_regex[0].indexOf('(')
                                     if position_opening_parenthesis isnt -1 and result_regex[0].slice(0, position_opening_parenthesis) of context
                                         # Save the var
-                                        # TODO Look for do() to get real_type
                                         element.real_type = 'json'
                                         element.type = 'var'
                                         element.name = result_regex[0].slice(0, position_opening_parenthesis)
@@ -748,8 +771,8 @@
                                         start = position_opening_parenthesis+1
                                         to_skip = result_regex[0].length-1

-
-                            result_regex = @regex.method_var.exec query.slice new_start # Check for method without parenthesis r., r.row., doc.
+                            # Check for method without parenthesis r., r.row., doc.
+                            result_regex = @regex.method_var.exec query.slice new_start
                             if result_regex isnt null
                                 if result_regex[0].slice(0, result_regex[0].length-1) of context
                                     element.type = 'var'
@@ -785,6 +808,7 @@
                                 start = i+result_regex[0].length-1+1
                                 to_skip = result_regex[0].length-1
                                 continue
+
                             # Look for a semi colon
                             result_regex = @regex.semicolon.exec query.slice i
                             if result_regex isnt null
@@ -802,12 +826,10 @@
                                 to_skip = result_regex[0].length-1
                                 continue
                         #else # if element.start isnt i
-                            # Skip white spaces
-                            # TODO
+                        # We caught the white spaces, so there is nothing to do here

                     else # element.type isnt null
-                        # White spaces can means a new start: r.table(...).eqJoin('id', r.table(...), 'other_id')
-                        # Is that even possible?
+                        # Catch separator like for groupedMapReduce
                         result_regex = @regex.comma.exec(query.slice(i))
                         if result_regex isnt null and stack_stop_char.length < 1
                             # element should have been pushed in stack. If not, the query is malformed
@@ -824,8 +846,7 @@
                             to_skip = result_regex[0].length-1
                             continue

-
-                        # Hum, if we reach here, the query is probably malformed of there's a bug here
+                        # Catch for anonymous function
                         else if element.type is 'anonymous_function'
                             if char of @stop_char.opening
                                 stack_stop_char.push char
@@ -843,9 +864,9 @@
                                             type: null
                                             context: context
                                         start = i+1
-                                #else something is broken here.
-                                #TODO Default behavior? The user forgot to close something?
-                                #@get_error_from_query is going to report this issue
+                                #else the written query is broken here. The user forgot to close something?
+                                #TODO Default behavior? Wait for Brackets/Ace to see how we handle errors
+                        # Catch for function
                         else if element.type is 'function'
                             if char of @stop_char.opening
                                 stack_stop_char.push char
@@ -863,6 +884,8 @@
                                             type: null
                                             context: context
                                         start = i+1
+
+                        # Catch for object
                         else if element.type is 'object'
                             # Since we are sure that we are not in a string, we can just look for colon and comma
                             # Still, we need to check the stack_stop_char since we can have { key: { inner: 'test, 'other_inner'}, other_key: 'other_value'}
@@ -887,11 +910,9 @@
                                             element.body[element.body.length-1] = new_element
                                         element.next_key = null # No more next_key
                                         element.complete = true
-                                        #else the next key is not defined
-                                        #TODO Check that the next_key is just white spaces, else "throw" an error
-                                        #if not element.next_key?
-                                        #    element.key_complete = false
-                                        #    element.next_key = query.slice element.current_key_start
+                                        # if not element.next_key?
+                                        # The next key is not defined, this is a broken query.
+                                        # TODO show error once brackets/ace will be used

                                         stack.push element
                                         element =
@@ -927,6 +948,7 @@
                                     to_skip = result_regex[0].length-1
                                     element.next_key = null
                                     element.current_key_start = i+result_regex[0].length
+                        # Catch for array
                         else if element.type is 'array'
                             if char of @stop_char.opening
                                 stack_stop_char.push char
@@ -958,6 +980,7 @@
                                     element.body.push new_element
                                 entry_start = i+1

+            # We just reached the end, let's try to find the type of the incomplete element
             if element.type isnt null
                 element.complete = false
                 if element.type is 'function'
@@ -1015,7 +1038,6 @@
                     element.name = query.slice start
                     element.complete = true
                 else if query[start] is '.'
-                    #TODO add check for [a-zA-Z]?
                     element.type = 'function'
                     element.position = position+start
                     element.name = query.slice start+1
@@ -1024,14 +1046,14 @@
                     element.name = query.slice start
                     element.position = position+start
                     element.complete = false
-                #if @regex.white.test(element.name) is false # If its type is null and the name is just white spaces, we ignore the element
                 stack.push element
             return stack

-        # TODO return type
+        # Decide if we have to show a suggestion or a description
+        # Mainly use the stack created by extract_data_from_query
         create_suggestion: (args) =>
             stack = args.stack
-            query = args.query # We don't need it anymore. Remove it later if it's really the case
+            query = args.query
             result = args.result

             # No stack, ie an empty query
@@ -1128,8 +1150,8 @@
                             for suggestion in @suggestions[@map_state[element.name]]
                                 if result.suggestions_regex.test(suggestion) is true
                                     result.suggestions.push suggestion
-                        else
-                            #TODO fully test the suggestion then when everything is cool, show a warning
+                        #else # This is a non valid ReQL function.
+                        # It may be a personalized function defined in the data explorer...
                         result.status = 'done'
                     else if element.type is 'var' and element.complete is true
                         result.state = element.real_type
@@ -1140,6 +1162,7 @@
                         result.status = 'done'
                     #else # Is that possible? A function can only be preceded by a function (except for r)

+        # Create regex based on the user input. We make it safe
         create_safe_regex: (str) =>
             for char in @unsafe_to_safe_regexstr
                 str = str.replace char.pattern, char.replacement
@@ -1147,8 +1170,8 @@

         # Return the first unmatched closing parenthesis/square bracket/curly bracket
+        # TODO use and show errors once we'll have decided how to handle errors. Brackets/ace may do it themselves.
         # TODO Check if a key is used more than once in an object
-        # TODO Talked to Slava, we may not use this functionnality, so let's just keep it commented for now
         ###
         get_errors_from_query: (query) =>
             is_string = false
@@ -1201,22 +1224,24 @@
                 return null
         ###

+        # Show suggestion and determine where to put the box
         show_suggestion: =>
             @move_suggestion()
-            #margin = (parseInt(@.$('.CodeMirror-cursor').css('top').replace('px', ''))+@line_height)+'px'
             margin = (parseInt(@.$('.CodeMirror-cursor').css('top').replace('px', ''))+@line_height)+'px'
             @.$('.suggestion_full_container').css 'margin-top', margin
             @.$('.arrow').css 'margin-top', margin

             @.$('.suggestion_name_list').show()
             @.$('.arrow').show()
-
+
+        # If want to show suggestion without moving the arrow
         show_suggestion_without_moving: =>
             @.$('.arrow').show()
             @.$('.suggestion_name_list').show()

+        # Show description and determine where to put it
         show_description: (fn) =>
-            if @descriptions[fn]?
+            if @descriptions[fn]? # Just for safety
                 margin = (parseInt(@.$('.CodeMirror-cursor').css('top').replace('px', ''))+@line_height)+'px'

                 @.$('.suggestion_full_container').css 'margin-top', margin
@@ -1247,6 +1272,7 @@
             else
                 @.$('.arrow').show()

+        # Move the suggestion. We have steps of 200 pixels and try not to overlaps button if we can. If we cannot, we just hide them all since their total width is less than 200 pixels
         move_suggestion: =>
             margin_left = parseInt(@.$('.CodeMirror-cursor').css('left').replace('px', ''))+23
             @.$('.arrow').css 'margin-left', margin_left
@@ -1303,11 +1329,7 @@
         mouseout_suggestion: (event) =>
             @hide_description()

-
-        #TODO refactor show_suggestion, show_suggestion_description, add_description
-
-        # Extend description for db() and table() with a list of databases or namespaces
-        # TODO refactor extend_description and extract_database_used. We can just use the stack built by extract_data_from_query
+        # Extend description for .db() and .table() with dbs/tables names
         extend_description: (fn) =>
             if @options?.can_extend? and @options?.can_extend is false
                 return @descriptions[fn]
@@ -1347,6 +1369,8 @@
                 description = @descriptions[fn]
             return description

+        # We could create a new stack with @extract_data_from_query, but that would be a more expensive for not that much
+        # We can not use the previous stack too since autocompletion doesn't validate the query until you hit enter (or another key than tab)
         extract_database_used: =>
             query_lines = @codemirror.getValue().split '\n'
             query_before_cursor = ''
@@ -1354,7 +1378,7 @@
                 for i in [0..@codemirror.getCursor().line-1]
                     query_before_cursor += query_lines[i] + '\n'
             query_before_cursor += query_lines[@codemirror.getCursor().line].slice 0, @codemirror.getCursor().ch
-            # TODO: check that there is not a string containing db( before...
+            # We cannot have ".db(" in a db name
             last_db_position = query_before_cursor.lastIndexOf('.db(')
             if last_db_position is -1
                 for database in databases.models
@@ -1444,6 +1468,7 @@
                 @.$('.loading_query_img').css 'display', 'none'
                 @results_view.render_error(@query, err)

+        # Callback for the query executed
         callback_multiple_queries: (data) =>
             # Check if the data sent by the server is an error
             if data instanceof rethinkdb.errors.RuntimeError or data instanceof rethinkdb.errors.BadQuery or data instanceof rethinkdb.errors.ClientError or data instanceof rethinkdb.errors.ClientError
@@ -1835,8 +1860,8 @@
                     sub_values.push
                         key: key
                         value: @json_to_node value[key]
-                    #TODO Remove this check by sending whether the comma should be add or not
-                    if typeof value[key] is 'string' and (/^(http|https):\/\/[^\s]+$/i.test(value[key]) or  /^[a-z0-9._-]+@[a-z0-9]+.[a-z0-9._-]{2,4}/i.test(value[key]))
+                    # We don't add a coma for url and emails, because we put it in value (value = url, >>)
+                    if typeof value[key] is 'string' and ((/^(http|https):\/\/[^\s]+$/i.test(value[key]) or /^[a-z0-9._-]+@[a-z0-9]+.[a-z0-9._-]{2,4}/i.test(value[key])))
                         sub_values[sub_values.length-1]['no_comma'] = true

                 if sub_values.length isnt 0
@@ -1970,8 +1995,6 @@
                     data['value'] = '[ ]'
                     data['classname'] = 'empty array'
                 else
-                    #TODO Build preview
-                    #TODO Add arrows for attributes
                     data['value'] = '[ ... ]'
                     data['data_to_expand'] = JSON.stringify(value)
             else if value_type is 'object'
@@ -1998,7 +2021,7 @@
             data = dom_element.data('json_data')
             result = @json_to_tree data
             dom_element.html result
-            classname_to_change = dom_element.parent().attr('class').split(' ')[0] #TODO Use a Regex
+            classname_to_change = dom_element.parent().attr('class').split(' ')[0]
             $('.'+classname_to_change).css 'max-width', 'none'
             classname_to_change = dom_element.parent().parent().attr('class')
             $('.'+classname_to_change).css 'max-width', 'none'
@@ -2018,7 +2041,6 @@

             return result

-        #TODO change cursor
         mouse_down: false
         handle_mousedown: (event) =>
             if event?.target?.className is 'click_detector'
@@ -2028,7 +2050,6 @@
                 @mouse_down = true
                 $('body').toggleClass('resizing', true)

-        #TODO Handle when last column is resized or when table expands too much
         handle_mousemove: (event) =>
             if @mouse_down
                 @last_columns_size[@col_resizing] = Math.max 5, @start_width-@start_x+event.pageX # Save the personalized size
@@ -2243,7 +2264,6 @@
         dataexplorer_toggle_history_template: Handlebars.templates['dataexplorer-toggle_history-template']
         className: 'history'

-        size_history: 100
         size_history_displayed: 300
         state: 'hidden' # hidden, visible
         index_displayed: 0


Put break condition in while loop

Put break condition in while loop

--- admin/static/coffee/dataexplorer.coffee
+++ admin/static/coffee/dataexplorer.coffee
@@ -93,9 +93,7 @@
                 if not @suggestions[element]?
                     @suggestions[element] = []
                 parent = relations[element]['parent']
-                while parent?
-                    if not suggestions[parent]?
-                        break
+                while parent? and suggestions[parent]
                     for suggestion in suggestions[parent]
                         @suggestions[element].push suggestion
                     parent = relations[parent]['parent']


FinderController:duplicateFiles remove async.parallel dependency, uses promises

--- client/Finder/filetree/controllers/findertreecontroller.coffee
+++ client/Finder/filetree/controllers/findertreecontroller.coffee
@@ -326,25 +326,25 @@

   duplicateFiles:(nodes, callback)->

-    stack = []
-    nodes.forEach (node)=>
-      stack.push (cb) =>
-        sourceItem = node.getData()
-        targetItem = @nodes[sourceItem.parentPath].getData()
-        FSItem.copy sourceItem, targetItem, (err, response)=>
-          if err then @notify null, null, err
-          else
-            cb err, node
-
-    callback or= (error, result) =>
-      @notify "#{result.length} item#{if result.length > 1 then 's' else ''} duplicated!", "success"
+    duplicatedNodes = []
+    results = nodes.map (node) =>
+      sourceItem = node.getData()
+      targetItem = @nodes[sourceItem.parentPath].getData()
+      FSItem.copy(sourceItem, targetItem).then ->
+        duplicatedNodes.push node
+
+    Promise.all(results).then =>
+      @notify "#{duplicatedNodes.length} item#{if duplicatedNodes.length > 1 then 's' else ''} duplicated!", "success"
       parentNodes = []
-      result.forEach (node)=>
+      duplicatedNodes.forEach (node)=>
         parentNode = @nodes[node.getData().parentPath]
         parentNodes.push parentNode unless parentNode in parentNodes
       @refreshFolder parentNode for parentNode in parentNodes

-    async.parallel stack, callback
+    .catch (err) =>
+      @notify null, null, err
+
+    .nodeify callback

   compressFiles:(nodeView, type)->


Workspace: Getting initial value for once. Removed logic to prevent recursion.

--- client/app/CommonViews/workspace/collaborativeworkspace.coffee
+++ client/app/CommonViews/workspace/collaborativeworkspace.coffee
@@ -8,27 +8,22 @@
     @firepadRef        = new Firebase "https://hulogggg.firebaseIO.com/"
     @sessionKey        = options.sessionKey or @createSessionKey()
     @workspaceRef      = @firepadRef.child @sessionKey
-    @isNewSession      = no
-    @isNeedToSave      = yes

-    @workspaceRef.on "value", (snapshot) =>
-      return unless @isNeedToSave
+    @workspaceRef.once "value", (snapshot) =>
       log "everything is something happened", "value", snapshot.val(), snapshot.name()

-      keys         = snapshot.val()?.keys
-      isOldSession = keys and not @isNewSession
+      keys = snapshot.val()?.keys

-      if isOldSession
+      if keys # if we have keys this means we're about to join an old session
         log "it's an old session, impressed!"
-        @isNewSession = no
         @sessionData  = keys
         @createPanel()
+        @workspaceRef.child("users").push KD.nick()
       else
         log "your awesome new session, saving keys now"
-        @isNewSession = yes
         @createPanel()
-        @isNeedToSave = no
         @workspaceRef.set "keys": @sessionData
+        @workspaceRef.child("users").push KD.nick()

     @workspaceRef.on "child_added", (snapshot) =>
       log "everything is something happened", "child_added", snapshot.val(), snapshot.name()


Remove unnecessary beforeEach from tests

--- spec/collector-spec.coffee
+++ spec/collector-spec.coffee
@@ -7,8 +7,6 @@
     subject = new Collector

   describe "getData", ->
-    beforeEach ->
-
     it "creates a request with the proper options", ->
       keys = _.keys(subject.getData())
       expect(keys).toContain 'user_agent'


Finished helper tests

Finished helper tests

--- spec/custom/helpers.coffee
+++ spec/custom/helpers.coffee
@@ -1,37 +1,101 @@
+fs = require 'fs'
 should = require 'should'
 helpers = require "../../#{process.env.SRC_DIR || 'src'}/helpers.js"

+
+describe 'helpers.compileCoffee', ->
+
+  it 'should compile coffeescript from a file', (done) ->
+    fs.writeFile 'test.coffee', 'x = 2', 'utf8', () ->
+      helpers.compileCoffee 'test.coffee', 'utf8', (err, code) ->
+        should.ifError(err)
+        code.should.include('var x;')
+        fs.unlink 'test.coffee', done
+
+  it 'should fail if the coffeescript is invalid', (done) ->
+    fs.writeFile 'test.coffee', '=====', 'utf8', () ->
+      helpers.compileCoffee 'test.coffee', 'utf8', (err, code) ->
+        err.toString().should.include('Parse error')
+        fs.unlink 'test.coffee', done
+
+  it 'should fail if the file does not exist', (done) ->
+    helpers.compileCoffee 'nonexisting.coffee', 'utf8', (err, code) ->
+      err.toString().should.include('no such file')
+      done()
+
+
+
+describe 'helpers.compileLess', ->
+
+  it 'should compile less from a file', (done) ->
+    fs.writeFile 'test.less', '@test: #ff0000; a { color: @test; }', 'utf8', () ->
+      helpers.compileLess 'test.less', [], 'utf8', (err, code) ->
+        should.ifError(err)
+        code.should.include('color: #ff0000')
+        fs.unlink 'test.less', done
+
+  it 'should fail if the coffeescript is invalid', (done) ->
+    fs.writeFile 'test.less', '=====', 'utf8', () ->
+      helpers.compileLess 'test.less', [], 'utf8', (err, code) ->
+        err.type.should.equal('Parse')
+        fs.unlink 'test.less', done
+
+  it 'should fail if the file does not exist', (done) ->
+    helpers.compileLess 'nonexisting.less', [], 'utf8', (err, code) ->
+      err.toString().should.include('no such file')
+      done()
+
+
+
 describe 'helpers.uglify', ->
+
   it 'should compress code', ->
     helpers.uglifier('1 + x').should.equal('1+x')
     helpers.uglifier('1 + 1').should.equal('2')
     helpers.uglifier('').should.equal('')
+
   it 'should default to an empty string', ->
     helpers.uglifier().should.equal('')

+
+
 describe 'helpers.endsWith', ->
+
   it 'should accept an array of arguments', ->
     helpers.endsWith('jakob', ['ab', 'cd']).should.be.false
     helpers.endsWith('jakob', ['ab', 'ob']).should.be.true
+
   it 'should not use regexps', ->
     helpers.endsWith('index.xhtml', ['.html']).should.be.false
     helpers.endsWith('index.xhtml', ['.xhtml']).should.be.true
+
   it 'should never match the empty set', ->
     helpers.endsWith('index.xhtml', []).should.be.false

+
+
 describe 'helpers.safeReplace', ->
+
   it 'should replace the first occurance of the string', ->
     helpers.safeReplace('abrakadabra', 'abra', '...').should.equal('...kadabra')
+
   it 'should return the original string if the substring does not exist', ->
     helpers.safeReplace('abrakadabra', 'banana', '...').should.equal('abrakadabra')

+
+
 describe 'helpers.safeReplaceAll', ->
+
   it 'should replace all occurance of the string', ->
     helpers.safeReplaceAll('abrakadabra', 'abra', '...').should.equal('...kad...')
+
   it 'should return the original string if the substring does not exist', ->
     helpers.safeReplaceAll('abrakadabra', 'banana', '...').should.equal('abrakadabra')

+
+
 describe 'helpers.execAll', ->
+
   it 'should return all match objects for the regexp', ->
     x = [['days', 'd'], ['later', 'l']]
     x[0].index = 3
@@ -39,31 +103,48 @@
     x[1].index = 8
     x[1].input = '28 days later'
     helpers.execAll(/([a-z])[a-z]+/g, '28 days later').should.eql(x)
+
   it 'should return an empty list if there are no matches', ->
     helpers.execAll(/([a-z])[a-z]+/g, '1337').should.eql([])

+
+
 describe 'helpers.arrayContains', ->
+
   it 'should return true if and only if the array contains the given element', ->
     helpers.arrayContains([1, 2, 3], 3).should.be.true
     helpers.arrayContains([1, 2, 3], 0).should.be.false

+
+
 describe 'helpers.createTag', ->
+
   it 'should create a proper html tag', ->
     helpers.createTag('a', { href: '#', id: 'test' }, 'Text').should.equal('Text')
+
   it 'should be able to create tags without content', ->
     helpers.createTag('a', { href: '#', id: 'test' }).should.equal('')
+
   it 'should default to no attributes', ->
     helpers.createTag('a').should.equal('')
+
   it 'should be possible to create content without attributes', ->
     helpers.createTag('a', 'content').should.equal('content')

+
+
 describe 'helpers.isPathAbsolute', ->
+
   it 'should check for initial slashes', ->
     helpers.isPathAbsolute('/test').should.be.true
     helpers.isPathAbsolute('bin').should.be.false

+
+
 describe 'helpers.escapeInlineScript', ->
+
   it 'should replace tag-opening with escape sequence', ->
     helpers.escapeInlineScript('script  script').should.equal('script


Testing which helpers are exposed

Testing which helpers are exposed

--- spec/custom/helpers.coffee
+++ spec/custom/helpers.coffee
@@ -1,6 +1,24 @@
 fs = require 'fs'
 should = require 'should'
 helpers = require "../../#{process.env.SRC_DIR || 'src'}/helpers.js"
+
+
+
+it "should expose 'serveConstructor'; nothing else", ->
+  helpers.should.have.keys [
+    'compileCoffee'
+    'compileLess'
+    'uglifier'
+    'endsWith'
+    'safeReplace'
+    'safeReplaceAll'
+    'execAll'
+    'arrayContains'
+    'createTag'
+    'isPathAbsolute'
+    'escapeInlineScript'
+  ]
+

 describe 'helpers.compileCoffee', ->


Inverted configuration

Inverted configuration

Updated the configuration for Pinned Tabs such that every settings is
now about enabling something instead of about disabling something,
making the configuration more intuitive to use.

--- lib/pinned-tabs.coffee
+++ lib/pinned-tabs.coffee
@@ -4,19 +4,19 @@
 module.exports = PinnedTabs =
     config:
         animated:
-            title: 'Disable animations'
-            description: 'Tick this to disable all animation related to Pinned Tabs'
+            title: 'Enable animations'
+            description: 'Tick this to enable all animation related to pinned tabs'
+            default: true
+            type: 'boolean'
+        closeUnpinned:
+            title: 'Enable the \'Close Unpinned Tabs\' option'
+            description: 'Tick this to show the \'Close Unpinned Tabs\' from the context menu'
             default: false
             type: 'boolean'
-        closeUnpinned:
-            title: 'Disable the \'Close Unpinned Tabs\' option'
-            description: 'Tick this to hide the \'Close Unpinned Tabs\' from the context menu'
+        modified:
+            title: 'Enable the modified icon on pinned tabs'
+            description: 'Tick this to enable the modified icon when hovering over pinned tabs'
             default: true
-            type: 'boolean'
-        modified:
-            title: 'Disable the modified icon on pinned tabs'
-            description: 'Tick this to disable the modified icon when hovering over pinned tabs'
-            default: false
             type: 'boolean'

     PinnedTabsState: undefined
@@ -177,12 +177,12 @@

     animated: (enable) ->
         body = document.querySelector 'body'
-        body.classList.toggle 'pinned-tabs-animated', !enable
+        body.classList.toggle 'pinned-tabs-animated', enable

     closeUnpinned: (enable) ->
         body = document.querySelector 'body'
-        body.classList.toggle 'close-unpinned', !enable
+        body.classList.toggle 'close-unpinned', enable

     modified: (enable) ->
         body = document.querySelector 'body'
-        body.classList.toggle 'pinned-tabs-modified', !enable
+        body.classList.toggle 'pinned-tabs-modified', enable


Remove fat arrow from TabBarView

Remove fat arrow from TabBarView

As this will always be bound to TabBarView for event listeners.

--- lib/tab-bar-view.coffee
+++ lib/tab-bar-view.coffee
@@ -217,7 +217,7 @@
   shouldAllowDrag: ->
     (@paneContainer.getPanes().length > 1) or (@pane.getItems().length > 1)

-  onDragStart: (event) =>
+  onDragStart: (event) ->
     return unless matches(event.target, '.sortable')

     event.dataTransfer.setData 'atom-event', 'true'
@@ -260,15 +260,15 @@
     catch error
       false

-  onDragLeave: (event) =>
+  onDragLeave: (event) ->
     @removePlaceholder()

-  onDragEnd: (event) =>
+  onDragEnd: (event) ->
     return unless matches(event.target, '.sortable')

     @clearDropTarget()

-  onDragOver: (event) =>
+  onDragOver: (event) ->
     unless event.dataTransfer.getData('atom-event') is 'true'
       event.preventDefault()
       event.stopPropagation()
@@ -297,7 +297,7 @@
         else
           element.parentElement.appendChild(placeholder)

-  onDropOnOtherWindow: (fromPaneId, fromItemIndex) =>
+  onDropOnOtherWindow: (fromPaneId, fromItemIndex) ->
     if @pane.id is fromPaneId
       if itemToRemove = @pane.getItems()[fromItemIndex]
         @pane.destroyItem(itemToRemove)
@@ -311,7 +311,7 @@
     @removeDropTargetClasses()
     @removePlaceholder()

-  onDrop: (event) =>
+  onDrop: (event) ->
     event.preventDefault()

     return unless event.dataTransfer.getData('atom-event') is 'true'
@@ -350,7 +350,7 @@

       atom.focus()

-  onMouseWheel: (event) =>
+  onMouseWheel: (event) ->
     return if event.shiftKey

     @wheelDelta ?= 0
@@ -363,7 +363,7 @@
       @wheelDelta = 0
       @pane.activatePreviousItem()

-  onMouseDown: (event) =>
+  onMouseDown: (event) ->
     return unless matches(event.target, ".tab")

     tab = closest(event.target, '.tab')
@@ -378,12 +378,12 @@
       @pane.destroyItem(tab.item)
       event.preventDefault()

-  onDoubleClick: (event) =>
+  onDoubleClick: (event) ->
     if event.target is this
       atom.commands.dispatch(this, 'application:new-file')
       event.preventDefault()

-  onClick: (event) =>
+  onClick: (event) ->
     return unless matches(event.target, ".tab .close-icon")

     tab = closest(event.target, '.tab')


Kill dead code

Kill dead code

--- lib/tab-bar-view.coffee
+++ lib/tab-bar-view.coffee
@@ -20,19 +20,6 @@
     @pane.on 'pane:active-item-changed', => @updateActiveTab()

     @updateActiveTab()
-
-#     @setActiveTab(@editor.getActiveEditSessionIndex())
-
-#     @editor.on 'editor:edit-session-added', (e, editSession) => @addTabForEditSession(editSession)
-#     @editor.on 'editor:edit-session-removed', (e, editSession, index) => @removeTabAtIndex(index)
-#     @editor.on 'editor:edit-session-order-changed', (e, editSession, fromIndex, toIndex) =>
-#       fromTab = @find(".tab:eq(#{fromIndex})")
-#       toTab = @find(".tab:eq(#{toIndex})")
-#       fromTab.detach()
-#       if fromIndex < toIndex
-#         fromTab.insertAfter(toTab)
-#       else
-#         fromTab.insertBefore(toTab)

     @on 'click', '.tab', (e) =>
       tab = $(e.target).closest('.tab').view()
@@ -81,13 +68,6 @@
   updateActiveTab: ->
     @setActiveTab(@tabForItem(@pane.activeItem))

-  removeTabAtIndex: (index) ->
-    @find(".tab:eq(#{index})").remove()
-
-  containsEditSession: (editor, editSession) ->
-    for session in editor.editSessions
-      return true if editSession.getPath() is session.getPath()
-
   shouldAllowDrag: ->
     (@paneContainer.getPanes().length > 1) or (@pane.getItems().length > 1)


updated openInNewWindow path logic

updated openInNewWindow path logic

updated openInNewWindow path logic to confirm valid path before closing tab
and opening new window with that path.

--- lib/tab-bar-view.coffee
+++ lib/tab-bar-view.coffee
@@ -140,14 +140,16 @@
   openInNewWindow: (tab) ->
     tab ?= @children('.right-clicked')[0]
     item = tab.item
+    return unless item?
+    if typeof item.getURI is 'function'
+      itemURI = item.getURI()
+    else if typeof item.getPath is 'function'
+      itemURI = item.getPath()
+    else if typeof item.getUri is 'function'
+      itemURI = item.getUri()
+    return unless itemURI?
+    atom.open({pathsToOpen: [itemURI], newWindow: true})
     @closeTab(tab)
-    if typeof item.getURI is 'function'
-      itemURI = item.getURI() ? ''
-    else if typeof item.getPath is 'function'
-      itemURI = item.getPath() ? ''
-    else if typeof item.getUri is 'function'
-      itemURI = item.getUri() ? ''
-    atom.open({pathsToOpen: [itemURI], newWindow: true})

   splitTab: (fn) ->
     if item = @children('.right-clicked')[0]?.item


Bind ::onDropOnOtherWindow to TabBarView

Fixes https://github.com/atom/tabs/issues/229

--- lib/tab-bar-view.coffee
+++ lib/tab-bar-view.coffee
@@ -82,10 +82,10 @@
     @addEventListener "dblclick", @onDoubleClick
     @addEventListener "click", @onClick

-    RendererIpc.on('tab:dropped', @onDropOnOtherWindow)
+    RendererIpc.on('tab:dropped', => @onDropOnOtherWindow(arguments...))

   unsubscribe: ->
-    RendererIpc.removeListener('tab:dropped', @onDropOnOtherWindow)
+    RendererIpc.removeListener('tab:dropped', => @onDropOnOtherWindow(arguments...))
     @subscriptions.dispose()

   handleTreeViewEvents: ->


More concrete commands

More concrete commands

--- lib/haskell-ghc-mod.coffee
+++ lib/haskell-ghc-mod.coffee
@@ -39,15 +39,16 @@
     @subscriptions_editor = new CompositeDisposable
     @editorMap = new WeakMap

-    @subscriptions_editor.add atom.commands.add 'atom-text-editor',
-      'haskell-ghc-mod:type': ({target}) =>
-        @editorMap.get(target.getModel())?.getType()
-      'haskell-ghc-mod:info': ({target}) =>
-        @editorMap.get(target.getModel())?.getInfo()
-      'haskell-ghc-mod:insert-type': ({target}) =>
-        @editorMap.get(target.getModel())?.insertType()
-      'haskell-ghc-mod:check': ({target}) =>
-        @editorMap.get(target.getModel())?.doCheck()
+    @subscriptions_editor.add \
+      atom.commands.add 'atom-text-editor[data-grammar~="haskell"]',
+        'haskell-ghc-mod:type': ({target}) =>
+          @editorMap.get(target.getModel())?.getType()
+        'haskell-ghc-mod:info': ({target}) =>
+          @editorMap.get(target.getModel())?.getInfo()
+        'haskell-ghc-mod:insert-type': ({target}) =>
+          @editorMap.get(target.getModel())?.insertType()
+        'haskell-ghc-mod:check': ({target}) =>
+          @editorMap.get(target.getModel())?.doCheck()

     @subscriptions_editor.add atom.workspace.observeTextEditors (editor) =>
       return unless editor.getGrammar().scopeName=="source.haskell"


Client: Sidebar: Added forceUpdate parameter to store handler.

--- client/app/lib/flux/environment/stores/activeinvitationmachineidstore.coffee
+++ client/app/lib/flux/environment/stores/activeinvitationmachineidstore.coffee
@@ -14,8 +14,9 @@
     @on actions.SET_ACTIVE_INVITATION_MACHINE_ID, @setMachineId

-  setMachineId: (activeMachineId, { id }) ->
+  setMachineId: (activeMachineId, { id, forceUpdate }) ->

+    return id  if forceUpdate
     return null  if activeMachineId is id
     return id


no need a pubInst here

no need a pubInst here

--- client/landing/app/MainApp/filetree/itemsubviews/finderitemrenameview.coffee
+++ client/landing/app/MainApp/filetree/itemsubviews/finderitemrenameview.coffee
@@ -8,7 +8,7 @@
       defaultValue  : data.name
       type          : "text"
       callback      : (newValue)=> @emit "FinderRenameConfirmation", newValue
-      keyup         : (pubInst, event)=>
+      keyup         : (event)=>
         @emit "FinderRenameConfirmation", (data.name) if event.which is 27

     @getSingleton("windowController").addLayer @input


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

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

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


Add tooltip

Add tooltip

--- lib/themes-panel.coffee
+++ lib/themes-panel.coffee
@@ -92,6 +92,8 @@
       @populateThemeMenus()

     @disposables.add atom.themes.onDidChangeActiveThemes => @updateActiveThemes()
+    @disposables.add atom.tooltips.add(@currentUiThemeSettings, {title: 'Settings'})
+    @disposables.add atom.tooltips.add(@currentSyntaxThemeSettings, {title: 'Settings'})
     @updateActiveThemes()

     @filterEditor.getModel().onDidStopChanging => @matchPackages()
@@ -173,6 +175,7 @@
       @currentSyntaxThemeSettings.hide()
     else
       @currentSyntaxThemeSettings.show()
+

   # Populate the theme menus from the theme manager's active themes
   populateThemeMenus: ->


shortcuts: make options an object by default, since commands are not binding to keyboard events anymoar

--- client/ide/lib/index.coffee
+++ client/ide/lib/index.coffee
@@ -555,11 +555,7 @@
       @openFile file, contents

-  createNewTerminal: (options) ->
-
-    # options can be an Event instance if the initiator is
-    # a shortcut so make the options an empty object.
-    options = {}  if options.keyCode
+  createNewTerminal: (options={}) ->

     { machine, path, resurrectSessions } = options


Logic moved.

Logic moved.

--- client/ide/lib/index.coffee
+++ client/ide/lib/index.coffee
@@ -783,9 +783,7 @@
     @forEachSubViewInIDEViews_ (view) ->
       if view instanceof Class
         if component is 'editor'
-          ace = view.aceView.ace
-          ace[method]? value
-          ace.editor.setFontSize value  if method is 'setFontSize'
+          view.getAce()[method]? value
         else
           view.webtermView.updateSettings()


Save scroll position on unmount as well

Save scroll position on unmount as well

--- src/smc-webapp/project_page.cjsx
+++ src/smc-webapp/project_page.cjsx
@@ -260,13 +260,25 @@
         group           : rtypes.string
         save_scroll     : rtypes.func

+    componentDidMount: ->
+        @restore_scroll_position()
+
+    componentWillUnmount: ->
+        @save_scroll_position()
+
     componentDidUpdate: ->
+        @restore_scroll_position()
+
+    componentWillUpdate: ->
+        @save_scroll_position()
+
+    restore_scroll_position: ->
         path         = misc.tab_to_path(@props.active_tab_name)
         saved_scroll = @props.open_files.getIn([path, 'component'])?.scroll_position
         if saved_scroll?
             @refs.editor_container.scrollTop = saved_scroll

-    componentWillUpdate: ->
+    save_scroll_position: ->
         if @refs.editor_container? and @props.save_scroll?
             val = @refs.editor_container.scrollTop
             @props.save_scroll(val)


ComputeProvider: make a strict check and also update credential field with username

--- workers/social/lib/social/models/computeproviders/computeprovider.coffee
+++ workers/social/lib/social/models/computeproviders/computeprovider.coffee
@@ -314,7 +314,12 @@
       oldDomain = "#{oldUsername}.#{KONFIG.userSitesDomain}"

       JMachine = require './machine'
-      JMachine.one domain: ///#{oldDomain}$///, (err, machine)->
+      JMachine.one
+
+        provider : 'koding'
+        domain   : ///#{oldDomain}$///
+
+      , (err, machine)->

         if err? or not machine
           console.log "Failed to find machine for #{username}", err
@@ -322,7 +327,15 @@
         else

           newDomain = "#{machine.uid}.#{username}.#{KONFIG.userSitesDomain}"
-          machine.update $set: domain: newDomain, (err)->
+
+          machine.update
+
+            $set         :
+              domain     : newDomain
+              credential : username
+
+          , (err)->
+
             unless err
               console.log """Machine domain updated for #{username}
                              from *.#{oldDomain} to #{newDomain}"""


Tests can be synchronous

Tests can be synchronous

Rendering of highlights is by default synchronous

--- spec/find-view-spec.coffee
+++ spec/find-view-spec.coffee
@@ -454,7 +454,6 @@
           waitsForPromise ->
             atom.workspace.open('sample.coffee')

-          waits 1
           runs ->
             # old editor has no more results
             expect(editorView.find('.highlight.find-result')).toHaveLength 0
@@ -529,7 +528,6 @@
           runs ->
             newEditorView = editorView.getPane().splitRight(newEditor)

-          waits 1
           runs ->
             # old editor has no more results
             expect(editorView.find('.find-result')).toHaveLength 0
@@ -551,7 +549,6 @@
             newEditorView.trigger('core:close')
             editorView.focus()

-          waits 1
           runs ->
             expect(editorView.find('.find-result')).toHaveLength 6

@@ -653,7 +650,6 @@
         findView.findEditor.setText 'notinthefilebro'
         findView.findEditor.trigger 'core:confirm'

-        waits 1
         runs ->
           expect(editorView.find('.find-result')).toHaveLength 0

@@ -668,22 +664,19 @@

         findView.findEditor.trigger 'core:confirm'
         findView.findEditor.trigger 'core:confirm'
-        waits 1
-        runs ->
-          nextResult = editorView.find('.current-result')
-          nextPosition = resultPosition(nextResult)
-          expect(nextResult).toHaveLength 1
-          expect(nextPosition).not.toEqual firstPosition
-
-          findView.findEditor.trigger 'find-and-replace:find-previous'
-          findView.findEditor.trigger 'find-and-replace:find-previous'
-
-        waits 1
-        runs ->
-          originalResult = editorView.find('.current-result')
-          originalPosition = resultPosition(originalResult)
-          expect(originalResult).toHaveLength 1
-          expect(originalPosition).toEqual firstPosition
+
+        nextResult = editorView.find('.current-result')
+        nextPosition = resultPosition(nextResult)
+        expect(nextResult).toHaveLength 1
+        expect(nextPosition).not.toEqual firstPosition
+
+        findView.findEditor.trigger 'find-and-replace:find-previous'
+        findView.findEditor.trigger 'find-and-replace:find-previous'
+
+        originalResult = editorView.find('.current-result')
+        originalPosition = resultPosition(originalResult)
+        expect(originalResult).toHaveLength 1
+        expect(originalPosition).toEqual firstPosition

       it "adds a class to the result when the current selection equals the result's range", ->
         originalResult = editorView.find('.current-result')
@@ -692,17 +685,13 @@

         editor.setSelectedBufferRange([[5, 16], [5, 20]])

-        waits 1
-        runs ->
-          expect(editorView.find('.current-result')).toHaveLength 0
-          editor.setSelectedBufferRange([[5, 16], [5, 21]])
-
-        waits 1
-        runs ->
-          newResult = editorView.find('.current-result')
-          newPosition = resultPosition(newResult)
-          expect(newResult).toHaveLength 1
-          expect(newPosition).not.toBe originalPosition
+        expect(editorView.find('.current-result')).toHaveLength 0
+        editor.setSelectedBufferRange([[5, 16], [5, 21]])
+
+        newResult = editorView.find('.current-result')
+        newPosition = resultPosition(newResult)
+        expect(newResult).toHaveLength 1
+        expect(newPosition).not.toBe originalPosition

     describe "when user types in the find editor", ->
       advance = ->


klient: Added a listener for the client.Subscribe Klient method

The `openFiles` event is triggered by `kdbin open foo`, to allow for
vm -> ide file opening. This listener, listens for that event.

Note: This is likely a WIP commit, because it's still under discussion.

--- client/ide/lib/index.coffee
+++ client/ide/lib/index.coffee
@@ -133,6 +133,7 @@

     @createInitialView withFakeViews
     @bindCollapseEvents()
+    @bindKlientEvents()

     {@finderPane, @settingsPane} = @workspace.panel.getPaneByName 'filesPane'

@@ -167,6 +168,29 @@

     baseSplit = panel.layout.getSplitViewByName 'BaseSplit'
     baseSplit.resizer.on 'dblclick', @bound 'toggleSidebar'
+
+
+  ###*
+   * Listen for any `clientSubscribe` events that we care about.
+   * Currently just `openFiles`, which triggers the IDE to open
+   * a new file.
+  ###
+  bindKlientEvents: ->
+
+    @once 'MachineDidMount', (machine) =>
+      kite = machine.getBaseKite()
+      kite.clientSubscribe
+        eventName : 'openFiles'
+        onPublish : ({ eventName, files }) =>
+          unless files
+            return kd.warn "bindKlientEvents-openFiles:
+              Files returned empty"
+
+          files.forEach (path) =>
+            file = FSHelper.createFileInstance { path, machine }
+            file.fetchContents yes, (err, content) =>
+              return kd.error err  if err
+              @openFile file, content

   bindWorkspaceDataEvents: ->


IDE: s/storageSetSynced/storageSetQueued

--- client/ide/lib/index.coffee
+++ client/ide/lib/index.coffee
@@ -693,7 +693,7 @@
     name  = @getWorkspaceSnapshotName()
     value = @getWorkspaceSnapshot()

-    @mountedMachine.getBaseKite().storageSetSynced name, value
+    @mountedMachine.getBaseKite().storageSetQueued name, value

   removeWorkspaceSnapshot: ->


Bug fix

Bug fix

--- fabricator.coffee
+++ fabricator.coffee
@@ -19,7 +19,7 @@
     models = Fabricator.new(model_type, count, attributes_info)
     queue = new Queue()
     for model in models
-      do (model) -> queue.defer (callback) -> models.save {}, adapters.bbCallback(callback)
+      do (model) -> queue.defer (callback) -> model.save {}, adapters.bbCallback(callback)
     queue.await (err) -> callback(err, models)

   @uniqueId: (prefix) -> return -> _.uniqueId(prefix or '')


Only call log when deprecations exist

Only call log when deprecations exist

--- spec/jasmine-helper.coffee
+++ spec/jasmine-helper.coffee
@@ -25,7 +25,9 @@
         log(str)
       onComplete: (runner) ->
         fs.closeSync(logStream) if logStream?
-        require('grim').logDeprecations() if process.env.JANKY_SHA1
+        if process.env.JANKY_SHA1
+          grim = require 'grim'
+          grim.logDeprecations() if grim.getDeprecationsLength() > 0
         atom.exit(runner.results().failedCount > 0 ? 1 : 0)
   else
     AtomReporter = require './atom-reporter'


Use sync FS for testing.

Use sync FS for testing.

--- spec/tree-view-spec.coffee
+++ spec/tree-view-spec.coffee
@@ -2438,6 +2438,7 @@
       fs.writeFileSync modifiedFile, 'ch ch changes'
       atom.project.getRepositories()[0].getPathStatus(modifiedFile)

+      treeView.useSyncFS = true
       treeView.updateRoots()
       $(treeView.roots[0].entries).find('.directory:contains(dir)')[0].expand()


Handle empty string valid lang, saved file...

--- lib/script-view.coffee
+++ lib/script-view.coffee
@@ -95,13 +95,12 @@
     # If no text was selected, either use the file
     # or select ALL the code in the editor

-    # File not saved, text not selected, "select" ALL the text
+    # Brand new file, text not selected, "select" ALL the text
     if (not @selectedText? or not @selectedText) and not @filepath?
       @selectedText = editor.getText()

     # If we still don't have selected text, use the path
-    # TODO: Totally borks on empty string, valid lang
-    if (not @selectedText? or not @selectedText)
+    if (not @selectedText? or not @selectedText) and @filepath?
       console.log("Down the file path")
       @filepath = editor.getPath()
       makeargs = grammarMap[@lang]["byFileArgs"]


Fix problem with template path

Fix problem with template path

--- app/static/script/app.coffee
+++ app/static/script/app.coffee
@@ -29,7 +29,7 @@
       $logProvider.debugEnabled false
       $routeProvider
         .when '/',
-          templateUrl: 'static/view/listing.html'
+          templateUrl: '/static/view/listing.html'
           controller: 'ListingCtrl'
         .otherwise
           redirectTo: '/'


edit message "Marks too close to each other" to "Marks may not overlap"

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -287,7 +287,7 @@
     return if @markingDisabled
     e.preventDefault()
     if @marks.markTooCloseToAnotherMark(e, @scale, @originalMin)
-      classifier.notify 'Marks too close to each other!'
+      classifier.notify 'Marks may not overlap!'
       @shakeGraph()
       return


Fix undefined connections

Fix undefined connections

--- app/js/controllers/footer.coffee
+++ app/js/controllers/footer.coffee
@@ -23,12 +23,13 @@
   on_update = (info) ->
     connections = info.network_connections
     $scope.connections = connections
-    if connections == 0
-      $scope.connections_str = "Not connected to the network"
+    if connections > 1
+      $scope.connections_str = "#{connections} network connections"
     else if connections == 1
       $scope.connections_str = "1 network connection"
     else
-      $scope.connections_str = "#{connections} network connections"
+      $scope.connections_str = "Not connected to the network"
+

     if connections < 4
       $scope.connections_img = "/img/signal_#{connections}.png"


Ace: Emitted file content related events.

--- client/app/Applications/Ace.kdapplication/AppView.coffee
+++ client/app/Applications/Ace.kdapplication/AppView.coffee
@@ -65,6 +65,15 @@
         @openSaveDialog()
       else
         @getData().emit "file.requests.save", contents
+
+    @ace.on "FileContentChanged", =>
+      @getActiveTabHandle().setClass "modified"
+
+    @ace.on "FileContentSynced", =>
+      @getActiveTabHandle().unsetClass "modified"
+
+  getActiveTabHandle: ->
+    return  @getDelegate().tabView.getActivePane().tabHandle

   preview: ->
     {vmName, path} = @getData()


Cleanup

Cleanup

--- lib/sourcefetch.coffee
+++ lib/sourcefetch.coffee
@@ -1,4 +1,7 @@
 {CompositeDisposable} = require 'atom'
+google = require 'google'
+request = require 'request'
+cheerio = require 'cheerio'

 # Returns the code from the first accepted SO answer
 # from the provided URLs to SO pages.
@@ -24,10 +27,8 @@
 # a StackOverflow page
 scrapeStackOverflow = (soLink) ->
   return new Promise (resolve, reject) ->
-    request = require 'request'
     request soLink, (error, response, body) ->
       if !error && response.statusCode == 200
-        cheerio = require 'cheerio'
         $ = cheerio.load body
         snippet = $('div.accepted-answer pre code').text()
         if snippet == ""
@@ -42,7 +43,6 @@
 # for the given query and language
 searchGoogle = (query, language) ->
   return new Promise (resolve, reject) ->
-    google = require "google"
     google.resultsPerPage = 10

     searchString = "#{query} in #{language} site:stackoverflow.com"
@@ -87,6 +87,6 @@
           atom.notifications.addSuccess "Got snippet!"
           editor.insertText(snippet)
         , (err) ->
-          atom.notifications.addError "FindSnippet error: " + JSON.stringify(err)
+          atom.notifications.addError err.reason
       , (err) ->
-        atom.notifications.addError "SearchGoogle error: " + JSON.stringify(err)
+        atom.notifications.addError err.reason


:memo: Explain how this ish works

:memo: Explain how this ish works

--- src/keymap-manager.coffee
+++ src/keymap-manager.coffee
@@ -411,6 +411,75 @@
   #
   # * `event` A `KeyboardEvent` of type 'keydown'
   handleKeyboardEvent: (event, {replay, disabledBindings}={}) ->
+    # Handling keyboard events is complicated and very nuanced. The
+    # complexity is all because of multi-stroke bindings. For example:
+    #
+    # 'ctrl-a b c': 'my-sweet-command'
+    #
+    # This example means the user can type `ctrl-a` then `b` then `c`, and after
+    # all of those keys are typed, it will dispatch the `my-sweet-command`
+    # command.
+    #
+    # The KeymapManager has a couple member variables to deal with multi-stroke
+    # bindings: `@queuedKeystrokes` and `@queuedKeyboardEvents`. They keep track
+    # of the keystrokes the user has typed, and every call to this function will
+    # try to match the user's keystrokes against bindings loaded in this object.
+    # When populated, the state variables look something like
+    #
+    # @queuedKeystrokes = ['ctrl-a', 'b', 'c']
+    # @queuedKeyboardEvents = [KeyboardEvent, KeyboardEvent, KeyboardEvent]
+    #
+    # Basically, this method will try to exactly match the user's keystrokes to
+    # a binding. If it cant match exactly, it looks for partial matches. A
+    # partial match might be something like:
+    #
+    # // The binding: 'ctrl-a b c': 'my-sweet-command'
+    # @queuedKeystrokes = ['ctrl-a', 'b']
+    #
+    # When it finds partially matching bindings, it will set the keymap into a
+    # pending state via `enterPendingState`.
+    #
+    # If a keystroke comes in that either matches a binding exactly, or yields
+    # no partial matches, we will reset the state variables and exit pending
+    # mode. If the keystroke yields no partial matches we will call
+    # `terminatePendingState`
+    #
+    # // Both of these will kick out of : 'ctrl-a b c': 'my-sweet-command'
+    # @queuedKeystrokes = ['ctrl-a', 'b', 'c'] // Exact match!
+    # @queuedKeystrokes = ['ctrl-a', 'b', 'd'] // No hope of matching, terminatePendingState()
+    #
+    # `terminatePendingState` is where things get crazy. Let's pretend the user
+    # typed `['ctrl-a', 'b', 'd']`. There are were no exact matches from this
+    # list of keystrokes, but there might be matches in a subset of the
+    # keystrokes. e.g.
+    #
+    # // These bindings all match a subset of ['ctrl-a', 'b', 'd']:
+    # 'ctrl-a': 'ctrl-a-command'
+    # 'b d': 'do-a-bd-deal'
+    # 'd o g': 'wag-the-dog'
+    #
+    # With these example commands, and the user's keystrokes, we should dispatch
+    # commands `ctrl-a-command` and `do-a-bd-deal`.
+    #
+    # To enable this behavior, `terminatePendingState` will _disable_ the
+    # original unmatched `ctrl-a b c` binding, empty the keystroke state
+    # variables, and _replay_ the events by running them through this
+    # `handleKeyboardEvent` function again. The replay acts exactly as if a user
+    # were typing the keys, but with a disabled binding. Because the original
+    # binding is disabled, the replayed keystrokes will match other, shorter
+    # bindings, and in this case, dispatch commands for our `ctrl-a` and then
+    # our `b d` bindings.
+    #
+    # Because the replay is calling this `handleKeyboardEvent` function again,
+    # it can get into another pending state, and again call
+    # `terminatePendingState`. The 2nd call to `terminatePendingState` might
+    # disable other bindings, and do another replay, which might call this
+    # function again ... and on and on. It will recurse until the KeymapManager
+    # is no longer in a pending state with no partial matches on the most recent
+    # event.
+    #
+    # Godspeed.
+
     keystroke = @keystrokeForKeyboardEvent(event)

     # We dont care about bare modifier keys in the bindings. e.g. `ctrl y` isnt going to work.


Refactor git-show.coffee

Refactor git-show.coffee

--- lib/models/git-show.coffee
+++ lib/models/git-show.coffee
@@ -3,7 +3,7 @@
 fs = require 'fs-plus'

 {CompositeDisposable} = require 'atom'
-{$, TextEditorView, View} = require 'atom-space-pen-views'
+{TextEditorView, View} = require 'atom-space-pen-views'

 git = require '../git'

@@ -19,23 +19,21 @@
     args.push '--'
     args.push file

-  git.cmd
-    args: args
-    cwd: repo.getWorkingDirectory()
-    stdout: (data) -> prepFile(data, objectHash) if data.length > 0
+  git.cmd(args, cwd: repo.getWorkingDirectory())
+  .then (data) -> prepFile(data, objectHash) if data.length > 0

 prepFile = (text, objectHash) ->
-  fs.writeFileSync showCommitFilePath(objectHash), text, flag: 'w+'
-  showFile(objectHash)
+  fs.writeFile showCommitFilePath(objectHash), text, flag: 'w+', (err) ->
+    if err then notifier.addError err else showFile objectHash

 showFile = (objectHash) ->
   disposables = new CompositeDisposable
   split = if atom.config.get('git-plus.openInPane') then atom.config.get('git-plus.splitPane')
   atom.workspace
     .open(showCommitFilePath(objectHash), split: split, activatePane: true)
-    .done (textBuffer) =>
+    .done (textBuffer) ->
       if textBuffer?
-        disposables.add textBuffer.onDidDestroy =>
+        disposables.add textBuffer.onDidDestroy ->
           disposables.dispose()
           try fs.unlinkSync showCommitFilePath(objectHash)


Fix adding collaborators

Fix adding collaborators

--- app/pages/lab/collaborators.cjsx
+++ app/pages/lab/collaborators.cjsx
@@ -172,5 +172,5 @@
         @setState saving: @state.saving

   handleCollaboratorAddition: ->
-    @project.uncacheLink 'project_roles'
+    @props.project.uncacheLink 'project_roles'
     @forceUpdate()


registration: nickname should not be disabled for guests

we now have a notion of a "half-registered" user, and we still need to collect a username from them after email confirmation

--- client/Account/views/username.coffee
+++ client/Account/views/username.coffee
@@ -33,7 +33,8 @@
           cssClass         : "thin"
           placeholder      : "username"
           name             : "username"
-          attributes       : readonly : "true"
+          attributes       :
+            readonly       : "#{not /^guest-/.test @account.profile.nickname}"
           testPath         : "account-username-input"
         password           :
           cssClass         : "thin half"


Klient: webtermKillSession: update session list when a session destroyed

--- client/Main/kite/kites/klient.coffee
+++ client/Main/kite/kites/klient.coffee
@@ -21,11 +21,11 @@
     fsCreateDirectory  : 'fs.createDirectory'

     webtermGetSessions : 'webterm.getSessions'
-    # webtermConnect     : 'webterm.connect'
-    webtermKillSession : 'webterm.killSession'
     webtermPing        : 'webterm.ping'

+
   constructor:->
+
     super

     @terminalSessions = []
@@ -88,6 +88,20 @@
         return remote

+  webtermKillSession: (options)->
+
+    {session} = options
+
+    @tell 'webterm.killSession', options
+
+      .then (state) =>
+
+        @terminalSessions = @terminalSessions.filter (currentSession)->
+          session isnt currentSession
+
+        return state
+
+
   fetchTerminalSessions: ->

     @webtermGetSessions()


Add count as a wrapped method. More TODOs.

--- src/document.coffee
+++ src/document.coffee
@@ -12,6 +12,8 @@
   constructor: (collection, mapping) ->
     Object.defineProperty this, 'collection', get: -> collection
     Object.defineProperty this, 'mapping', get: -> mapping
+    # TODO Jake: We also need to figure out how to handle default values and
+    # embedded documents... soon.
     # Create the attributes on the "class" level, which map to keys
     for attr, key of @mapping
       Object.defineProperty this, attr,
@@ -49,6 +51,7 @@
     findAndModify: value: ->
     insert: value: ->
     update: value: ->
+    count: value: ->

     # MongoJS methods that don't return documents
     save: get: -> @collection.save.bind @collection


keep inside

keep inside

--- src/layout/sprites.coffee
+++ src/layout/sprites.coffee
@@ -64,10 +64,12 @@
     my.assert dir?, "expects dir"
     position = world.get('position')
     direction = world.get('direction')
-    sum = if dir > 0 then vector.add(position, direction) else
-      vector.subtract(position, direction)
-    result = vector.bound sum, cell_count, -> world.send 'error'
-    world.put 'position', result
+    if dir > 0
+      sum = vector.add(position, direction)
+    else
+      sum = vector.subtract(position, direction)
+    valid = vector.inside sum, cell_count
+    world.put 'position', sum

   turn: (world, dir) ->
     my.assert dir?, "expects dir"


FIX Typo

FIX Typo

--- client/app/components/compose.coffee
+++ client/app/components/compose.coffee
@@ -340,7 +340,7 @@
                 composeInHTML: @props.settings.get 'composeInHTML'
                 isNew: false
             if (not message.get('html')?) and message.get('text')
-                state.conposeInHTML = false
+                state.composeInHTML = false

             # TODO : smarter ?
             state[key] = value for key, value of message.toJS()


Client: Use keydown for enter, not keyup. No more lag.

--- client/app/js/views/chat_view.coffee
+++ client/app/js/views/chat_view.coffee
@@ -4,7 +4,7 @@

   events:
     'click': 'focusChat'
-    'keyup .chat_input': 'sendChatIfEnter'
+    'keydown .chat_input': 'sendChatIfEnter'
     'click .chat_input_send': 'sendChat'
     'scroll_to_bottom': 'scrollToBottom'


fixing typo.

fixing typo.

--- app/routers/get-tweet-png.coffee
+++ app/routers/get-tweet-png.coffee
@@ -12,7 +12,7 @@
         height: "all"

     if req.params.tweet is "last"
-      if req.query.withReply == 'true'
+      if req.query.withReplies == 'true'
         url = "http://" + req.headers.host + "/" + req.params.username + "/last?withReplies=true"
       else
         url = "http://" + req.headers.host + "/" + req.params.username + "/last?withReplies=false"


cleanup continue

cleanup continue

--- lib/text-object.coffee
+++ lib/text-object.coffee
@@ -239,10 +239,8 @@

   getPointToSearchFrom: (selection, searchFrom) ->
     switch searchFrom
-      when 'head'
-        swrap(selection).getNormalizedBufferPosition()
-      when 'start'
-        swrap(selection).getBufferPositionFor('start')
+      when 'head' then swrap(selection).getNormalizedBufferPosition()
+      when 'start' then swrap(selection).getBufferPositionFor('start')

   # Allow override @allowForwarding by 2nd argument.
   getRange: (selection, options={}) ->
@@ -257,7 +255,7 @@
     pairInfo?.targetRange

   selectTextObject: (selection) ->
-    swrap(selection).setBufferRangeSafely @getRange(selection)
+    swrap(selection).setBufferRangeSafely(@getRange(selection))

 # -------------------------
 class AnyPair extends Pair
@@ -719,7 +717,7 @@
     @vimState.mark.getRange('[', ']')

   selectTextObject: (selection) ->
-    swrap(selection).setBufferRangeSafely @getRange()
+    swrap(selection).setBufferRangeSafely(@getRange())

 class ALatestChange extends LatestChange
   @extend()


Cleanup main.coffee

Cleanup main.coffee

--- app/main.coffee
+++ app/main.coffee
@@ -3,37 +3,32 @@
 translate.load enUs

 $ = window.jQuery
+
 require 'spine'
+Route = require 'spine/lib/route'
+{ Stack } = require 'spine/lib/manager'
+
+Api = require 'zooniverse/lib/api'
+User = require 'zooniverse/models/user'
+
+api = new Api project: 'wisconsin'
+
+TopBar = require 'zooniverse/controllers/top-bar'
+topBar = new TopBar
+topBar.el.appendTo 'body'

 Navigation = require './controllers/navigation'
+navigation = new Navigation
+navigation.el.appendTo  'body'

-Route = require 'spine/lib/route'
-AboutPage = require './controllers/about_page'
-HomePage = require './controllers/home_page'
-Classifier = require './controllers/classifier'
-Profile = require './controllers/profile'
-Api = require 'zooniverse/lib/api'
-Project  = require 'zooniverse/models/project'
-
-TopBar = require 'zooniverse/controllers/top-bar'
-Footer = require 'zooniverse/controllers/footer'
-User = require 'zooniverse/models/user'
-
-{ Stack } = require 'spine/lib/manager'
-ContentPage = require './controllers/content_page'
-feedbackContent = require './views/feedback_page'
-
-app = {}
-api = new Api project: 'wisconsin'
-
-app.stack = new Stack
+stack = new Stack
   className: "main #{Stack::className}"

   controllers:
-    home: HomePage
-    about: AboutPage
-    classify: Classifier
-    profile: Profile
+    home: require './controllers/home_page'
+    about: require './controllers/about_page'
+    classify: require './controllers/classifier'
+    profile: require './controllers/profile'

   routes:
     '/home': 'home'
@@ -43,22 +38,15 @@

   default: 'home'

+stack.el.appendTo  'body'
+
+Footer = require 'zooniverse/controllers/footer'
+footer = new Footer
+siteFooter = $('

').append footer.el
+siteFooter.appendTo 'body'
+
 Route.setup()
-
 User.fetch()

-app.topBar = new TopBar
-app.footer = new Footer
-
-navigation = new Navigation
-app.navigation = navigation
-
-app.navigation.el.appendTo  'body'
-app.stack.el.appendTo  'body'
-app.topBar.el.appendTo 'body'
-
-siteFooter = $('

').append app.footer.el
-siteFooter.appendTo 'body'
-
-window.app = app
+window.app = { stack, api }
 module.exports = window.app


assign socket for client connections

assign socket for client connections

--- lib/transformer.coffee
+++ lib/transformer.coffee
@@ -2,6 +2,10 @@
 module.exports = (options) ->

   (socket) ->
+    # NOTE:
+    #  net.Server's 'connection' event provides the new socket to the listener
+    #  net.Socket's 'connect' event's `this` *is* the socket
+    socket ?= this

     # if transform must be built, then build it...
     transform =


Fix missing names for Ida/Nando

Fix missing names for Ida/Nando

--- app/models/ThangType.coffee
+++ app/models/ThangType.coffee
@@ -255,6 +255,8 @@
       "Samurai": "Hattori"
       "Sorcerer": "Pender"
       "Trapper": "Senick"
+      "Champion": "Ida"
+      "Duelist": "Nando"
     }
     map[@get('name')]


:construction: Fix Post-Merge Conflicts

:construction: Fix Post-Merge Conflicts

--- lib/typechecker.coffee
+++ lib/typechecker.coffee
@@ -18,7 +18,7 @@
           clearTimeout ScrollTimeout
           ScrollTimeout = setTimeout(@OnScroll.bind(this),100)
       Subscriptions.push atom.workspace.observeTextEditors (editor)=>
-	    Grammar = editor.getGrammar()
+        Grammar = editor.getGrammar()
         return unless Grammar or Grammar.name isnt 'PHP' or Grammar.name isnt 'Hack' or Grammar.name isnt 'C++'
         editor.buffer.onDidSave (info)=>
           @Lint()
@@ -53,15 +53,11 @@
             throw new Error(result.stderr.substr(7)); # Throw an error that can not be caught
           ,0
           return ;
-<<<<<<< HEAD
-        result = JSON.parse(result.stderr.substr(result.stderr.indexOf('{')))
-=======
         try
           result = JSON.parse(result.stderr.substr(result.stderr.indexOf('{')))
         catch error
           console.log "Invalid JSON"
           console.log result
->>>>>>> revert
         Errors = result.errors
         if Main.V.H.config.type is 'remote'
           Errors.forEach (ErrorEntry)->
@@ -89,13 +85,8 @@
           LeErrors.push new Main.V.TE(I,TraceEntry.path,TraceEntry.line,TraceEntry.start,TraceEntry.end,Color,Error.message)
           LeFirst = false
       setTimeout =>
-<<<<<<< HEAD
-		try @OnScroll.bind(this)
-	  ,70
-=======
         try @OnScroll.call(this)
       ,70
->>>>>>> revert
     @OnScroll:->
       RowStart = EditorView.getFirstVisibleScreenRow()
       RowEnd = EditorView.getLastVisibleScreenRow()


Registration: add logic for enforcing login

--- client/Main/KD.extend.coffee
+++ client/Main/KD.extend.coffee
@@ -26,6 +26,7 @@
         warn "Removing the old one. It was ", KD.appClasses[options.name]
         @unregisterAppClass options.name

+    options.enforceLogin  ?= no           # a Boolean
     options.multiple      ?= no           # a Boolean
     options.background    ?= no           # a Boolean
     options.hiddenHandle  ?= no           # a Boolean
@@ -40,10 +41,16 @@
     options.routes       or= null         # <string> or <Object{slug: string, handler: function}>
     options.styles       or= []           # <Array<string>> list of stylesheets

+    enforceLogin=->
+      return if KD.isLoggedIn()
+      appManager = KD.getSingleton "appManager"
+      appManager.tell "Account", "showRegistirationNeededModal"
+
     wrapHandler = (fn, options) -> ->
       router = KD.getSingleton 'router'
       router.setPageTitle? options.navItem.title  if options.navItem.title
       fn.apply this, arguments
+      enforceLogin()  if options.enforceLogin

     registerRoute = (route, handler)=>
       slug        = if "string" is typeof route then route else route.slug
@@ -61,6 +68,7 @@
             else
               ({params:{name}, query})->
                 router.openSection options.name, name, query
+                enforceLogin()  if options.enforceLogin

           router.addRoute slug, handler


Test game with mocha

Test game with mocha

--- src/load/test_game.coffee
+++ src/load/test_game.coffee
@@ -1,22 +1,19 @@
 {god} = require('../god')
 {load} = require('../load')

-exports.test_game = (test, rx) ->
+exports.test_game = ->
   example = load(rx, {game: 'example', level: 1})
+  describe 'Game', ->
+    it 'loads', ->
+      assert.ok example, 'example'
+      assert.ok example.is_world(example), 'is world'

-  test 'game load', (t) ->
-    t.ok example, 'example'
-    t.ok example.is_world(example), 'is world'
-    t.end()
+    it 'lists', ->
+      assert.ok list = load(rx, {list: 'test'}), 'list'
+      assert.ok example.is_world(list), 'list is world'

-  test 'game list', (t) ->
-    t.ok list = load(rx, {list: 'test'}), 'list'
-    t.ok example.is_world(list), 'list is world'
-    t.end()
+    it '#next_params', ->
+      assert.ok params = example.get('next_params'), 'next_params'
+      assert.equal params.level, 2, 'level'
+      assert.ok url = example.get('next_url'), 'next_url'

-  test 'game next_params', (t) ->
-    t.ok params = example.get('next_params'), 'next_params'
-    t.equal params.level, 2, 'level'
-    t.ok url = example.get('next_url'), 'next_url'
-    t.end()
-


added missing ldap settings

added missing ldap settings

--- server-ce/settings.coffee
+++ server-ce/settings.coffee
@@ -480,7 +480,11 @@
 		adminDN: process.env["SHARELATEX_LDAP_ADMIN_DN"]
 		adminPW: process.env["SHARELATEX_LDAP_ADMIN_PW"]
 		starttls:  process.env["SHARELATEX_LDAP_TLS"] or false
-		tlsOptions:
+		nameAtt: process.env["SHARELATEX_LDAP_NAME_ATT"]
+		lastNameAtt: process.env["SHARELATEX_LDAP_LAST_NAME_ATT"]
+
+	if process.env["SHARELATEX_LDAP_TLS_OPTS_CA_PATH"]
+		settings.ldap.tlsOptions =
 			rejectUnauthorized: process.env["SHARELATEX_LDAP_TLS_OPTS_REJECT_UNAUTH"] or false
 			ca: process.env["SHARELATEX_LDAP_TLS_OPTS_CA_PATH"] # e.g.'/etc/ldap/ca_certs.pem'


Check if file exists before reading

Check if file exists before reading

--- lib/pdf-editor-view.coffee
+++ lib/pdf-editor-view.coffee
@@ -102,8 +102,11 @@
       @updatePdf()

   updatePdf: ->
+    @fileChanged = false
+
+    return unless fs.existsSync(@filePath)
+
     @updating = true
-    @fileChanged = false
     @container.find("canvas").remove()
     @canvases = []


Fixed the file not 'changing' when overwritten. It was a caching issue.

--- server/routes/file.coffee
+++ server/routes/file.coffee
@@ -130,6 +130,8 @@
       errors.conflict(res, {canForce:userCanEditFile(req.user, file)})
       done(true)
     else if file
+      fullPath = "/file/#{options.metadata.path}/#{options.filename}"
+      clearCloudFlareCacheForFile(fullPath)
       q = { _id: file._id }
       q.root = 'media'
       Grid.gfs.remove q, (err) ->
@@ -172,3 +174,16 @@
   options.metadata.description = req.body.description if req.body.description?

   options
+
+clearCloudFlareCacheForFile = (path='/file') ->
+  request = require 'request'
+  r = request.post 'https://www.cloudflare.com/api_json.html', (err, httpResponse, body) ->
+    if (err)
+      console.error('CloudFlare file cache clear failed:', body)
+
+  form = r.form()
+  form.append 'tkn', 'dea38682b209901a014dba2b2702afa5476a0'
+  form.append 'email', 'scott@codecombat.com'
+  form.append 'z', 'codecombat.com'
+  form.append 'a', 'zone_file_purge'
+  form.append 'url', "http://codecombat.com#{path}"


Update pause menu with stats

Update pause menu with stats

--- app/scripts/lib/PauseMenu.coffee
+++ app/scripts/lib/PauseMenu.coffee
@@ -7,6 +7,7 @@
     Crafty.bind 'GamePause', (state) =>
       if state
         @createMenu()
+        @showPlayers()
       else
         @remove()

@@ -116,6 +117,59 @@
       @bind 'Fire', ->
         executeSelection()

+  showPlayers: ->
+    Crafty('Player').each ->
+      return unless @ship
+      xOff = .05
+      xOff = .70 if @playerNumber is 2
+
+      Crafty.e('2D, WebGL, playerShip, ColorEffects, PauseMenu')
+        .attr(
+          w: 71
+          h: 45
+          x: - Crafty.viewport.x + ((xOff + .07) * Crafty.viewport.width)
+          y: (Crafty.viewport.height * .3) - Crafty.viewport.y + 20
+          z: 101
+        )
+        .colorOverride?(@color(), 'partial')
+      statList = [
+        "Score: #{@points}"
+        "Lives: #{@lives - 1}"
+        ""
+        "Speed:     +#{@ship.primaryWeapon.stats.speed}"
+        "RapidFire: +#{@ship.primaryWeapon.stats.rapid}"
+        "AimAssist: +#{@ship.primaryWeapon.stats.aim}"
+        "Damage:    +#{@ship.primaryWeapon.stats.damage}"
+      ]
+      stats = Crafty.e('2D, WebGL, Color, PauseMenu')
+        .attr(
+          x: - Crafty.viewport.x + (xOff * Crafty.viewport.width)
+          y: (Crafty.viewport.height * .3) - Crafty.viewport.y
+          w: (.25 * Crafty.viewport.width)
+          h: (statList.length + 5) * 20
+          z: 100
+          alpha: .3
+        )
+        .color('#000')
+      for o, i in statList
+        stat= Crafty.e('2D, DOM, Text')
+          .attr(
+            x: stats.x + 20
+            y: stats.y + 85 + (20 * i)
+            w: stats.w - 60
+            z: 110
+          )
+          .text(o)
+          .textColor('#D0D0D0')
+          .css("textAlign", "left")
+          .textFont(
+            size: '8px'
+            weight: 'bold'
+            family: 'Press Start 2P'
+          )
+        stats.attach stat
+
+
   remove: ->
     Crafty('Player').each ->
       @unbind 'Up'


sad hack... wtf Travis :'(

sad hack... wtf Travis :'(

--- components/clock/view.coffee
+++ components/clock/view.coffee
@@ -21,7 +21,7 @@
       success: =>
         @model.on('change:clockState', ->
           clearInterval @interval
-          location.reload()
+          location?.reload()
         )
         @render()


Default port for livereload (#557)

Default port for livereload (#557)

--- gulp/tasks/watch.coffee
+++ gulp/tasks/watch.coffee
@@ -4,7 +4,7 @@

 gulp.task 'reload', false, ->
-  do $.livereload.listen
+  $.livereload.listen 35729
   gulp.watch([
     "#{paths.static.dev}/**/*.{css,js}"
     "#{paths.main}/**/*.{html,py}"


Add Comments for access

Add Comments for access

--- lib/status-bar-view.coffee
+++ lib/status-bar-view.coffee
@@ -20,33 +20,42 @@

     @storeActiveBuffer()

+  # Private:
   attach: ->
     atom.rootView.vertical.append(this) unless @hasParent()

+  # Public:
   appendLeft: (item) ->
     @leftPanel.append(item)

+  # Public:
   appendRight: (item) ->
     @rightPanel.append(item)

+  # Public:
   getActiveBuffer: ->
     @buffer

+  # Public:
   getActiveItem: ->
     atom.rootView.getActivePaneItem()

+  # Private:
   storeActiveBuffer: ->
     @buffer = @getActiveItem()?.getBuffer?()

+  # Public:
   subscribeToBuffer: (event, callback) ->
     @bufferSubscriptions.push([event, callback])
     @buffer.on(event, callback) if @buffer

+  # Priavte:
   subscribeAllToBuffer: ->
     return unless @buffer
     for [event, callback] in @bufferSubscriptions
       @buffer.on(event, callback)

+  # Priavte:
   unsubscribeAllFromBuffer: ->
     return unless @buffer
     for [event, callback] in @bufferSubscriptions


Change the pusher channel name so it can be parsed unambigously, if the user or channel contain -

--- js/inner.coffee
+++ js/inner.coffee
@@ -381,7 +381,7 @@
       @build_num

   pusherChannel: () =>
-    "private-#{@project_name()}-#{@build_num}".replace(/\//g,"-")
+    "private-#{@project_name()}@#{@build_num}".replace(/\//g,"@")

   update: (json) =>
     @status(json.status)


missed committing couple of methods for Project in inner.js

--- js/inner.coffee
+++ js/inner.coffee
@@ -658,18 +658,20 @@
     else
       if l.vcs_url().toLowerCase() > r.vcs_url().toLowerCase() then 1 else -1

-  paid_parallelism: =>
-    if @paying_user() then @paying_user().parallelism else 1
-
-  # load_paying_user: =>
-  #   if not @paying_user()?
-  #     $.ajax
-  #       type: "GET"
-  #       url: "/api/v1/project/#{@project_name()}/paying_user"
-  #       success: (result)
-  #         @paying_user(new User(result))
-  #         true
-
+  speed_description_style: =>
+    'selected-label': @focused_parallel() == @parallel()
+
+  disable_parallel_input: (num) =>
+    num > @max_parallelism()
+
+  load_paying_user: =>
+    $.ajax
+      type: "GET"
+      url: "/api/v1/project/#{@project_name()}/paying_user"
+      success: (result) =>
+        if result?
+          @paying_user(new User(result))
+          @loaded_paying_user(true)

   checkbox_title: =>
     "Add CI to #{@project_name()}"
@@ -812,7 +814,6 @@
       url: "/api/v1/project/#{@project_name()}/invite/#{user.login}"

   refresh: () =>
-    @paying_user(null)
     $.getJSON "/api/v1/project/#{@project_name()}/settings", (data) =>
       @updateObservables(data)

@@ -824,7 +825,9 @@
       url: "/api/v1/project/#{@project_name()}/settings"
       data: JSON.stringify
         parallel: @parallel()
-
+      error: (data) =>
+        @refresh()
+        @load_paying_user()
     true # TODO: make this reflect what is happening on the server

   parallel_input_id: (num) =>
@@ -1087,6 +1090,10 @@
   parallelism_cost: (plan, p) =>
     @raw_parallelism_cost(p) - @raw_parallelism_cost(plan.min_parallelism)

+  # p2 > p1
+  parallelism_cost_difference: (plan, p1, p2) =>
+    @parallelism_cost(plan, p2) - @parallelism_cost(plan, p1)
+
   concurrency_cost: (plan, c) ->
     if plan.concurrency == "Unlimited"
       0


Append newline to w3c outputs

Append newline to w3c outputs

--- src/streammachine/log_controller.coffee
+++ src/streammachine/log_controller.coffee
@@ -109,7 +109,7 @@
             #console.log "in w3c logger with ", level, msg, meta, cb

             # for a valid w3c log, level should == "request", meta.
-            logline = "#{meta.ip} #{meta.ip} #{strftime(meta.time,"%F %T")} #{meta.path} #{meta.bytes} #{meta.seconds}"
+            logline = "#{meta.ip} #{meta.ip} #{strftime(meta.time,"%F %T")} #{meta.path} 200 #{meta.bytes} #{meta.seconds}\n"

             if @file
                 # make sure there aren't any queued writes
@@ -127,7 +127,6 @@

                 @queued.push logline
                 cb null, true
-

         #----------


Additional confirmations for VM view

Additional confirmations for VM view

--- app/modules/vm/index.coffee
+++ app/modules/vm/index.coffee
@@ -265,39 +265,59 @@
       }

     $scope.stopVM = (id) ->
-      xo.vm.stop id
-      notify.info {
-        title: 'VM shutdown...'
-        message: 'Gracefully shutdown the VM'
-      }
+      modal.confirm
+        title: 'VM shutdown'
+        message: 'Are you sure you want to shutdown this VM ?'
+      .then ->
+        xo.vm.stop id
+        notify.info {
+          title: 'VM shutdown...'
+          message: 'Gracefully shutdown the VM'
+        }

     $scope.force_stopVM = (id) ->
-      xo.vm.stop id, true
-      notify.info {
-        title: 'VM force shutdown...'
-        message: 'Force shutdown the VM'
-      }
+      modal.confirm
+        title: 'VM force shutdown'
+        message: 'Are you sure you want to force shutdown for this VM ?'
+      .then ->
+        xo.vm.stop id, true
+        notify.info {
+          title: 'VM force shutdown...'
+          message: 'Force shutdown the VM'
+        }

     $scope.rebootVM = (id) ->
-      xo.vm.restart id
-      notify.info {
-        title: 'VM reboot...'
-        message: 'Gracefully reboot the VM'
-      }
+      modal.confirm
+        title: 'VM reboot'
+        message: 'Are you sure you want to reboot this VM ?'
+      .then ->
+        xo.vm.restart id
+        notify.info {
+          title: 'VM reboot...'
+          message: 'Gracefully reboot the VM'
+        }

     $scope.force_rebootVM = (id) ->
-      xo.vm.restart id, true
-      notify.info {
-        title: 'VM reboot...'
-        message: 'Force reboot the VM'
-      }
+      modal.confirm
+        title: 'VM reboot'
+        message: 'Are you sure you want to force reboot for this VM ?'
+      .then ->
+        xo.vm.restart id, true
+        notify.info {
+          title: 'VM reboot...'
+          message: 'Force reboot the VM'
+        }

     $scope.suspendVM = (id) ->
-      xo.vm.suspend id
-      notify.info {
-        title: 'VM suspend...'
-        message: 'Suspend the VM'
-      }
+      modal.confirm
+        title: 'VM suspend'
+        message: 'Are you sure you want to suspend this VM ?'
+      .then ->
+        xo.vm.suspend id
+        notify.info {
+          title: 'VM suspend...'
+          message: 'Suspend the VM'
+        }

     $scope.resumeVM = (id) ->
       xo.vm.resume id, true
@@ -486,8 +506,12 @@
     $scope.disconnectVBD = (vdi) ->
       id = resolveVBD(vdi)
       if id?
-        console.log "Disconnect VBD #{id}"
-        xo.vbd.disconnect id
+        modal.confirm({
+          title: 'VBD disconnection'
+          message: 'Are you sure you want to detach this VM disk ?'
+        }).then ->
+          console.log "Disconnect VBD #{id}"
+          xo.vbd.disconnect id

     $scope.connectVBD = (vdi) ->
       id = resolveVBD(vdi)
@@ -498,11 +522,11 @@
     $scope.deleteVBD = (vdi) ->
       id = resolveVBD(vdi)
       if id?
-        console.log "Delete VBD #{id}"
         modal.confirm({
           title: 'VBD deletion'
           message: 'Are you sure you want to delete this VM disk attachment (the disk will NOT be destroyed)?'
         }).then ->
+          console.log "Delete VBD #{id}"
           xo.vbd.delete id

     $scope.connectVIF = (id) ->
@@ -511,9 +535,13 @@
       xo.vif.connect id

     $scope.disconnectVIF = (id) ->
-      console.log "Disconnect VIF #{id}"
-
-      xo.vif.disconnect id
+      modal.confirm
+        title: 'Disconnect VIF'
+        message: 'Are you sure you want to disconnect this interface ?'
+      .then ->
+        console.log "Disconnect VIF #{id}"
+
+        xo.vif.disconnect id

     $scope.deleteVIF = (id) ->
       console.log "Delete VIF #{id}"