import sublime import re import os # TODO: # - Document this file. # - Split out functionality where possible. # This file is what happens when you code non-stop for several days. # I tried to make the main files as easy to follow along as possible. # This file, not so much. # Set to true to enable debug output DEBUG = False SETTINGS_FILE_NAME = "CoffeeComplete Plus.sublime-settings" PREFERENCES_COFFEE_EXCLUDED_DIRS = "coffee_autocomplete_plus_excluded_dirs" PREFERENCES_COFFEE_RESTRICTED_TO_PATHS = "coffee_autocomplete_plus_restricted_to_paths" PREFERENCES_THIS_ALIASES = "coffee_autocomplete_plus_this_aliases" PREFERENCES_MEMBER_EXCLUSION_REGEXES = "coffee_autocomplete_plus_member_exclusion_regexes" BUILT_IN_TYPES_SETTINGS_FILE_NAME = "CoffeeComplete Plus Built-In Types.sublime-settings" BUILT_IN_TYPES_SETTINGS_KEY = "coffee_autocomplete_plus_built_in_types" CUSTOM_TYPES_SETTINGS_FILE_NAME = "CoffeeComplete Plus Custom Types.sublime-settings" CUSTOM_TYPES_SETTINGS_KEY = "coffee_autocomplete_plus_custom_types" FUNCTION_RETURN_TYPES_SETTINGS_KEY = "coffee_autocomplete_plus_function_return_types" FUNCTION_RETURN_TYPE_TYPE_NAME_KEY = "type_name" FUNCTION_RETURN_TYPE_FUNCTION_NAMES_KEY = "function_names" COFFEESCRIPT_SYNTAX = r"CoffeeScript" COFFEE_EXTENSION_WITH_DOT = "\.coffee|\.litcoffee|\.coffee\.md" CONSTRUCTOR_KEYWORD = "constructor" THIS_SUGAR_SYMBOL = "@" THIS_KEYWORD = "this" PERIOD_OPERATOR = "." COFFEE_FILENAME_REGEX = r".+?" + re.escape(COFFEE_EXTENSION_WITH_DOT) CLASS_REGEX = r"class\s+%s((\s*$)|[^a-zA-Z0-9_$])" CLASS_REGEX_ANY = r"class\s+([a-zA-Z0-9_$]+)((\s*$)|[^a-zA-Z0-9_$])" CLASS_REGEX_WITH_EXTENDS = r"class\s+%s\s*($|(\s+extends\s+([a-zA-Z0-9_$.]+)))" SINGLE_LINE_COMMENT_REGEX = r"#.*?$" TYPE_HINT_COMMENT_REGEX = r"#.*?\[([a-zA-Z0-9_$]+)\].*$" TYPE_HINT_PARAMETER_COMMENT_REGEX = r"#.*?(\[([a-zA-Z0-9_$]+)\]\s*{var_name}((\s*$)|[^a-zA-Z0-9_$]))|({var_name}\s*\[([a-zA-Z0-9_$]+)\]((\s*$)|[^a-zA-Z0-9_$]))" # Function regular expression. Matches: # methodName = (aas,bsa, casd ) -> FUNCTION_REGEX = r"(^|[^a-zA-Z0-9_$])(%s)\s*[:]\s*(\((.*?)\))?\s*[=\-]>" FUNCTION_REGEX_ANY = r"(^|[^a-zA-Z0-9_$])(([a-zA-Z0-9_$]+))\s*[:]\s*(\((.*?)\))?\s*[=\-]>" # Assignment regular expression. Matches: # asdadasd = ASSIGNMENT_REGEX = r"(^|[^a-zA-Z0-9_$])%s\s*=" # Static assignment regex STATIC_ASSIGNMENT_REGEX = r"^\s*([@]|(this\s*[.]))\s*([a-zA-Z0-9_$]+)\s*[:=]" # Static function regex STATIC_FUNCTION_REGEX = r"(^|[^a-zA-Z0-9_$])\s*([@]|(this\s*[.]))\s*([a-zA-Z0-9_$]+)\s*[:]\s*(\((.*?)\))?\s*[=\-]>" # Regex for finding a function parameter. Call format on the string, with name=var_name PARAM_REGEX = r"\(\s*(({name})|({name}\s*=?.*?[,].*?)|(.*?[,]\s*{name}\s*=?.*?[,].*?)|(.*?[,]\s*{name}))\s*=?.*?\)\s*[=\-]>" # Regex for finding a variable declared in a for loop. FOR_LOOP_REGEX = r"for\s*.*?[^a-zA-Z0-9_$]%s[^a-zA-Z0-9_$]" # Regex for constructor @ params, used for type hinting. CONSTRUCTOR_SELF_ASSIGNMENT_PARAM_REGEX = r"constructor\s*[:]\s*\(\s*((@{name})|(@{name}\s*[,].*?)|(.*?[,]\s*@{name}\s*[,].*?)|(.*?[,]\s*@{name}))\s*\)\s*[=\-]>\s*$" # Assignment with the value it's being assigned to. Matches: # blah = new Dinosaur() ASSIGNMENT_VALUE_WITH_DOT_REGEX = r"(^|[^a-zA-Z0-9_$])%s\s*=\s*(.*)" ASSIGNMENT_VALUE_WITHOUT_DOT_REGEX = r"(^|[^a-zA-Z0-9_$.])%s\s*=\s*(.*)" # Used to determining what class is being created with the new keyword. Matches: # new Macaroni NEW_OPERATION_REGEX = r"new\s+([a-zA-Z0-9_$.]+)" PROPERTY_INDICATOR = u'\u25CB' METHOD_INDICATOR = u'\u25CF' INHERITED_INDICATOR = u'\u2C75' BUILT_IN_TYPES_TYPE_NAME_KEY = "name" BUILT_IN_TYPES_TYPE_ENABLED_KEY = "enabled" BUILT_IN_TYPES_CONSTRUCTOR_KEY = "constructor" BUILT_IN_TYPES_STATIC_PROPERTIES_KEY = "static_properties" BUILT_IN_TYPES_STATIC_PROPERTY_NAME_KEY = "name" BUILT_IN_TYPES_STATIC_METHODS_KEY = "static_methods" BUILT_IN_TYPES_STATIC_METHOD_NAME_KEY = "name" BUILT_IN_TYPES_INSTANCE_PROPERTIES_KEY = "instance_properties" BUILT_IN_TYPES_INSTANCE_PROPERTY_NAME_KEY = "name" BUILT_IN_TYPES_INSTANCE_METHODS_KEY = "instance_methods" BUILT_IN_TYPES_INSTANCE_METHOD_NAME_KEY = "name" BUILT_IN_TYPES_METHOD_NAME_KEY = "name" BUILT_IN_TYPES_METHOD_INSERTION_KEY = "insertion" BUILT_IN_TYPES_METHOD_ARGS_KEY = "args" BUILT_IN_TYPES_METHOD_ARG_NAME_KEY = "name" BUILT_IN_TYPES_INHERITS_FROM_OBJECT_KEY = "inherits_from_object" # Utility functions def debug(message): if DEBUG: print message def select_current_word(view): if len(view.sel()) > 0: selected_text = view.sel()[0] word_region = view.word(selected_text) view.sel().clear() view.sel().add(word_region) def get_selected_word(view): word = "" if len(view.sel()) > 0: selected_text = view.sel()[0] word_region = view.word(selected_text) word = get_word_at(view, word_region) return word def get_word_at(view, region): word = "" word_region = view.word(region) word = view.substr(word_region) word = re.sub(r'[^a-zA-Z0-9_$]', '', word) word = word.strip() return word def get_token_at(view, region): token = "" if len(view.sel()) > 0: selected_line = view.line(region) preceding_text = view.substr(sublime.Region(selected_line.begin(), region.begin())).strip() token_regex = r"[^a-zA-Z0-9_$.@]*?([a-zA-Z0-9_$.@]+)$" match = re.search(token_regex, preceding_text) if match: token = match.group(1) token = token.strip() return token def get_preceding_symbol(view, prefix, locations): index = locations[0] symbol_region = sublime.Region(index - 1 - len(prefix), index - len(prefix)) symbol = view.substr(symbol_region) return symbol def get_preceding_function_call(view): function_call = "" if len(view.sel()) > 0: selected_text = view.sel()[0] selected_line = view.line(sublime.Region(selected_text.begin() - 1, selected_text.begin() - 1)) preceding_text = view.substr(sublime.Region(selected_line.begin(), selected_text.begin() - 1)).strip() function_call_regex = r".*?([a-zA-Z0-9_$]+)\s*\(.*?\)" match = re.search(function_call_regex, preceding_text) if match: function_call = match.group(1) return function_call def get_preceding_token(view): token = "" if len(view.sel()) > 0: selected_text = view.sel()[0] if selected_text.begin() > 2: token_region = sublime.Region(selected_text.begin() - 1, selected_text.begin() - 1) token = get_token_at(view, token_region) return token # Complete this. def get_preceding_call_chain(view): word = "" if len(view.sel()) > 0: selected_text = view.sel()[0] selected_text = view.sel()[0] selected_line = view.line(sublime.Region(selected_text.begin() - 1, selected_text.begin() - 1)) preceding_text = view.substr(sublime.Region(selected_line.begin(), selected_text.begin() - 1)).strip() function_call_regex = r".*?([a-zA-Z0-9_$]+)\s*\(.*?\)" match = re.search(function_call_regex, preceding_text) if match: #function_call = match.group(1) pass return word def is_capitalized(word): capitalized = False # Underscores are sometimes used to indicate an internal property, so we # find the first occurrence of an a-zA-Z character. If not found, we assume lowercase. az_word = re.sub("[^a-zA-Z]", "", word) if len(az_word) > 0: first_letter = az_word[0] capitalized = first_letter.isupper() # Special case for $ capitalized = capitalized | word.startswith("$") return capitalized def get_files_in(directory_list, filename_regex, excluded_dirs): files = [] for next_directory in directory_list: # http://docs.python.org/2/library/os.html?highlight=os.walk#os.walk for path, dirs, filenames in os.walk(next_directory): # print str(path) for next_excluded_dir in excluded_dirs: try: dirs.remove(next_excluded_dir) except: pass for next_file_name in filenames: # http://docs.python.org/2/library/re.html match = re.search(filename_regex, next_file_name) if match: # http://docs.python.org/2/library/os.path.html?highlight=os.path.join#os.path.join next_full_path = os.path.join(path, next_file_name) files.append(next_full_path) return files def get_lines_for_file(file_path): lines = [] try: # http://docs.python.org/2/tutorial/inputoutput.html opened_file = open(file_path, "r") # r = read only lines = opened_file.readlines() except: pass return lines # Returns a tuple with (row, column, match, row_start_index), or None def get_positions_of_regex_match_in_file(file_lines, regex): found_a_match = False matched_row = -1 matched_column = -1 match_found = None line_start_index = -1 current_row = 0 current_line_start_index = 0 for next_line in file_lines: # Remove comments modified_next_line = re.sub(SINGLE_LINE_COMMENT_REGEX, "", next_line) match = re.search(regex, modified_next_line) if match: found_a_match = True matched_row = current_row matched_column = match.end() match_found = match line_start_index = current_line_start_index break current_row = current_row + 1 current_line_start_index = current_line_start_index + len(next_line) positions_tuple = None if found_a_match: positions_tuple = (matched_row, matched_column, match_found, line_start_index) return positions_tuple def open_file_at_position(window, file_path, row, column): # Beef # http://www.sublimetext.com/docs/2/api_reference.html#sublime.Window path_with_position_encoding = file_path + ":" + str(row) + ":" + str(column) window.open_file(path_with_position_encoding, sublime.ENCODED_POSITION) return # Returns a tuple with (file_path, row, column, match, row_start_index) def find_location_of_regex_in_files(contents_regex, local_file_lines, global_file_path_list=[]): # The match tuple containing the filename and positions. # Will be returned as None if no matches are found. file_match_tuple = None if local_file_lines: # Search the file for the regex. positions_tuple = get_positions_of_regex_match_in_file(local_file_lines, contents_regex) if positions_tuple: # We've found a match! Save the file path plus the positions and the match itself file_match_tuple = tuple([None]) + positions_tuple # If we are to search globally... if not file_match_tuple and global_file_path_list: for next_file_path in global_file_path_list: if next_file_path: file_lines = get_lines_for_file(next_file_path) # Search the file for the regex. positions_tuple = get_positions_of_regex_match_in_file(file_lines, contents_regex) if positions_tuple: # We've found a match! Save the file path plus the positions and the match itself file_match_tuple = tuple([next_file_path]) + positions_tuple # Stop the for loop break return file_match_tuple def select_region_in_view(view, region): view.sel().clear() view.sel().add(region) # Refresh hack. original_position = view.viewport_position() view.set_viewport_position((original_position[0], original_position[1] + 1)) view.set_viewport_position(original_position) def get_progress_indicator_tuple(previous_indicator_tuple): STATUS_MESSAGE_PROGRESS_INDICATOR = "[%s=%s]" if not previous_indicator_tuple: previous_indicator_tuple = ("", 0, 1) progress_indicator_position = previous_indicator_tuple[1] progress_indicator_direction = previous_indicator_tuple[2] # This animates a little activity indicator in the status area. # It animates an equals symbol bouncing back and fourth between square brackets. # We calculate the padding around the equal based on the last known position. num_spaces_before = progress_indicator_position % 8 num_spaces_after = (7) - num_spaces_before # When the equals hits the edge, we change directions. # Direction is -1 for moving left and 1 for moving right. if not num_spaces_after: progress_indicator_direction = -1 if not num_spaces_before: progress_indicator_direction = 1 progress_indicator_position += progress_indicator_direction padding_before = ' ' * num_spaces_before padding_after = ' ' * num_spaces_after # Create the progress indication text progress_indicator_text = STATUS_MESSAGE_PROGRESS_INDICATOR % (padding_before, padding_after) # Return the progress indication tuple return (progress_indicator_text, progress_indicator_position, progress_indicator_direction) def get_syntax_name(view): syntax = os.path.splitext(os.path.basename(view.settings().get('syntax')))[0] return syntax def is_coffee_syntax(view): return bool(re.match(COFFEESCRIPT_SYNTAX, get_syntax_name(view))) def get_this_type(file_lines, start_region): type_found = None # Search backwards from current position for the type # We're looking for a class definition class_regex = CLASS_REGEX_ANY match_tuple = search_backwards_for(file_lines, class_regex, start_region) if match_tuple: # debug(str(match_tuple[0]) + ", " + str(match_tuple[1]) + ", " + match_tuple[2].group(1)) type_found = match_tuple[2].group(1) else: debug("No match!") return type_found def get_variable_type(file_lines, token, start_region, global_file_path_list, built_in_types, previous_variable_names=[]): type_found = None # Check for "this" if token == "this": type_found = get_this_type(file_lines, start_region) elif token.startswith("@"): token = "this." + token[1:] # We're looking for a variable assignent assignment_regex = ASSIGNMENT_VALUE_WITH_DOT_REGEX % token # print "Assignment regex: " + assignment_regex # Search backwards from current position for the type if not type_found: match_tuple = search_backwards_for(file_lines, assignment_regex, start_region) if match_tuple: type_found = get_type_from_assignment_match_tuple(token, match_tuple, file_lines, previous_variable_names) # Well, we found the assignment. But we don't know what it is. # Let's try to find a variable name and get THAT variable type... if not type_found: type_found = get_type_from_assigned_variable_name(file_lines, token, match_tuple, global_file_path_list, built_in_types, previous_variable_names) # Let's try searching backwards for parameter hints in comments... if not type_found: # The regex used to search for the variable as a parameter in a method param_regex = PARAM_REGEX.format(name=re.escape(token)) match_tuple = search_backwards_for(file_lines, param_regex, start_region) # We found the variable! it's a parameter. Let's find a comment with a type hint. if match_tuple: type_found = get_type_from_parameter_match_tuple(token, match_tuple, file_lines, previous_variable_names) # If backwards searching isn't working, at least try to find something... if not type_found: # Forward search from beginning for assignment: match_tuple = get_positions_of_regex_match_in_file(file_lines, assignment_regex) if match_tuple: type_found = get_type_from_assignment_match_tuple(token, match_tuple, file_lines, previous_variable_names) if not type_found: type_found = get_type_from_assigned_variable_name(file_lines, token, match_tuple, global_file_path_list, built_in_types, previous_variable_names) # If still nothing, maybe it's an @ parameter in the constructor? if not type_found: # Get the last word in the chain, if it's a chain. # E.g. Get variableName from this.variableName.[autocomplete] selected_word = token[token.rfind(".") + 1:] if token.startswith(THIS_KEYWORD + ".") or token.startswith(THIS_SUGAR_SYMBOL): # The regex used to search for the variable as a parameter in a method param_regex = CONSTRUCTOR_SELF_ASSIGNMENT_PARAM_REGEX.format(name=re.escape(selected_word)) # Forward search from beginning for param: match_tuple = get_positions_of_regex_match_in_file(file_lines, param_regex) # We found the variable! it's a parameter. Let's find a comment with a type hint. if match_tuple: type_found = get_type_from_parameter_match_tuple(selected_word, match_tuple, file_lines) if not type_found: # Find something. Anything! word_assignment_regex = ASSIGNMENT_VALUE_WITHOUT_DOT_REGEX % selected_word # Forward search from beginning for assignment: match_tuple = get_positions_of_regex_match_in_file(file_lines, word_assignment_regex) if match_tuple: type_found = get_type_from_assignment_match_tuple(token, match_tuple, file_lines, previous_variable_names) if not type_found: type_found = get_type_from_assigned_variable_name(file_lines, token, match_tuple, global_file_path_list, built_in_types, previous_variable_names) return type_found def get_type_from_assigned_variable_name(file_lines, token, match_tuple, global_file_path_list, built_in_types, previous_variable_names=[]): type_found = None assignment_value_string = match_tuple[2].group(2).strip() # row start index + column index token_index = match_tuple[3] + match_tuple[1] token_region = sublime.Region(token_index, token_index) token_match = re.search(r"^([a-zA-Z0-9_$.]+)$", assignment_value_string) if token_match: next_token = token_match.group(1) if next_token not in previous_variable_names: previous_variable_names.append(token) type_found = get_variable_type(file_lines, next_token, token_region, global_file_path_list, built_in_types, previous_variable_names) # Determine what type a method returns if not type_found: # print "assignment_value_string: " + assignment_value_string method_call_regex = r"([a-zA-Z0-9_$.]+)\s*[.]\s*([a-zA-Z0-9_$]+)\s*\(" method_call_match = re.search(method_call_regex, assignment_value_string) if method_call_match: object_name = method_call_match.group(1) method_name = method_call_match.group(2) object_type = get_variable_type(file_lines, object_name, token_region, global_file_path_list, built_in_types, previous_variable_names) if object_type: type_found = get_return_type_for_method(object_type, method_name, file_lines, global_file_path_list, built_in_types) return type_found def get_return_type_for_method(object_type, method_name, file_lines, global_file_path_list, built_in_types): type_found = None next_class_to_scan = object_type # Search the class and all super classes while next_class_to_scan and not type_found: class_regex = CLASS_REGEX % re.escape(next_class_to_scan) # (file_path, row, column, match, row_start_index) class_location_search_tuple = find_location_of_regex_in_files(class_regex, file_lines, global_file_path_list) if class_location_search_tuple: file_found = class_location_search_tuple[0] # Consider if it was found locally, in the view if not file_found: class_file_lines = file_lines else: class_file_lines = get_lines_for_file(file_found) # If found, search for the method in question. method_regex = FUNCTION_REGEX % re.escape(method_name) positions_tuple = get_positions_of_regex_match_in_file(class_file_lines, method_regex) # (row, column, match, row_start_index) if positions_tuple: # Check for comments, and hopefully the return hint, on previous rows. matched_row = positions_tuple[0] row_to_check_index = matched_row - 1 non_comment_code_reached = False while not non_comment_code_reached and row_to_check_index >= 0 and not type_found: current_row_text = class_file_lines[row_to_check_index] # Make sure this line only contains comments. mod_line = re.sub(SINGLE_LINE_COMMENT_REGEX, "", current_row_text).strip() # If it wasn't just a comment line... if len(mod_line) > 0: non_comment_code_reached = True else: # Search for hint: @return [TYPE] return_type_hint_regex = r"@return\s*\[([a-zA-Z0-9_$]+)\]" hint_match = re.search(return_type_hint_regex, current_row_text) if hint_match: # We found it! type_found = hint_match.group(1) row_to_check_index = row_to_check_index - 1 # If nothing was found, see if the class extends another one and is inheriting the method. if not type_found: extends_regex = CLASS_REGEX_WITH_EXTENDS % next_class_to_scan # (row, column, match, row_start_index) extends_match_positions = get_positions_of_regex_match_in_file(class_file_lines, extends_regex) if extends_match_positions: extends_match = extends_match_positions[2] next_class_to_scan = extends_match.group(3) else: next_class_to_scan = None return type_found def get_type_from_assignment_match_tuple(variable_name, match_tuple, file_lines, previous_variable_names=[]): type_found = None if match_tuple: match = match_tuple[2] assignment_value_string = match.group(2) # Check for a type hint on current row or previous row. # These will override anything else. matched_row = match_tuple[0] previous_row = matched_row - 1 current_row_text = file_lines[matched_row] hint_match = re.search(TYPE_HINT_COMMENT_REGEX, current_row_text) if hint_match: type_found = hint_match.group(1) if not type_found and previous_row >= 0: previous_row_text = file_lines[previous_row] hint_match = re.search(TYPE_HINT_COMMENT_REGEX, previous_row_text) if hint_match: type_found = hint_match.group(1) if not type_found: assignment_value_string = re.sub(SINGLE_LINE_COMMENT_REGEX, "", assignment_value_string).strip() type_found = get_type_from_assignment_value(assignment_value_string) return type_found def get_type_from_parameter_match_tuple(variable_name, match_tuple, file_lines, previous_variable_names=[]): type_found = None if match_tuple: # Check for comments, and hopefully type hints, on previous rows. matched_row = match_tuple[0] row_to_check_index = matched_row - 1 non_comment_code_reached = False while not non_comment_code_reached and row_to_check_index >= 0 and not type_found: current_row_text = file_lines[row_to_check_index] # Make sure this line only contains comments. mod_line = re.sub(SINGLE_LINE_COMMENT_REGEX, "", current_row_text).strip() # If it wasn't just a comment line... if len(mod_line) > 0: non_comment_code_reached = True else: # It's a comment. Let's look for a type hint in the form: # variable_name [TYPE] ~OR~ [TYPE] variable_name hint_regex = TYPE_HINT_PARAMETER_COMMENT_REGEX.format(var_name=re.escape(variable_name)) hint_match = re.search(hint_regex, current_row_text) if hint_match: # One of these two groups contains the type... if hint_match.group(2): type_found = hint_match.group(2) else: type_found = hint_match.group(6) row_to_check_index = row_to_check_index - 1 return type_found def get_type_from_assignment_value(assignment_value_string): determined_type = None assignment_value_string = assignment_value_string.strip() # Check for built in types object_regex = r"^\{.*\}$" if not determined_type: match = re.search(object_regex, assignment_value_string) if match: determined_type = "Object" double_quote_string_regex = r"(^\".*\"$)|(^.*?\+\s*\".*?\"$)|(^\".*?\"\s*\+.*?$)|(^.*?\s*\+\s*\".*?\"\s*\+\s*.*?$)" if not determined_type: match = re.search(double_quote_string_regex, assignment_value_string) if match: determined_type = "String" single_quote_string_regex = r"(^['].*[']$)|(^.*?\+\s*['].*?[']$)|(^['].*?[']\s*\+.*?$)|(^.*?\s*\+\s*['].*?[']\s*\+\s*.*?$)" if not determined_type: match = re.search(single_quote_string_regex, assignment_value_string) if match: determined_type = "String" array_regex = r"^\[.*\]\s*$" if not determined_type: match = re.search(array_regex, assignment_value_string) if match: determined_type = "Array" boolean_regex = r"^(true)|(false)$" if not determined_type: match = re.search(boolean_regex, assignment_value_string) if match: determined_type = "Boolean" # http://stackoverflow.com/questions/4703390/how-to-extract-a-floating-number-from-a-string-in-python number_regex = r"^[-+]?\d*\.\d+|\d+$" if not determined_type: match = re.search(number_regex, assignment_value_string) if match: determined_type = "Number" regexp_regex = r"^/.*/[a-z]*$" if not determined_type: match = re.search(regexp_regex, assignment_value_string) if match: determined_type = "RegExp" new_operation_regex = NEW_OPERATION_REGEX if not determined_type: match = re.search(new_operation_regex, assignment_value_string) if match: determined_type = get_class_from_end_of_chain(match.group(1)) return determined_type # Tuple returned: (matched_row, matched_column, match, row_start_index) def search_backwards_for(file_lines, regex, start_region): matched_row = -1 matched_column = -1 match_found = None row_start_index = -1 start_index = start_region.begin() # debug("start: " + str(start_index)) characters_consumed = 0 start_line = -1 indentation_size = 0 current_line_index = 0 for next_line in file_lines: # Find the line we're starting on... offset = start_index - characters_consumed if offset <= len(next_line) + 1: # debug("Start line: " + next_line) characters_consumed = characters_consumed + len(next_line) indentation_size = get_indentation_size(next_line) start_line = current_line_index break characters_consumed = characters_consumed + len(next_line) current_line_index = current_line_index + 1 row_start_index = characters_consumed if start_line >= 0: # debug("start line: " + str(start_line)) # Go backwards, searching for the class definition. for i in reversed(range(start_line + 1)): previous_line = file_lines[i] # print "Next line: " + previous_line[:-1] row_start_index = row_start_index - len(previous_line) # debug("Line " + str(i) + ": " + re.sub("\n", "", previous_line)) # Returns -1 for empty lines or lines with comments only. next_line_indentation = get_indentation_size(previous_line) #debug("Seeking <= indentation_size: " + str(indentation_size) + ", Current: " + str(next_line_indentation)) # Ignore lines with larger indentation sizes and empty lines (or lines with comments only) if next_line_indentation >= 0 and next_line_indentation <= indentation_size: indentation_size = next_line_indentation # Check for the class match = re.search(regex, previous_line) if match: matched_row = i matched_column = match.end() match_found = match break match_tuple = None if match_found: match_tuple = (matched_row, matched_column, match_found, row_start_index) return match_tuple def get_indentation_size(line_of_text): size = -1 mod_line = re.sub("\n", "", line_of_text) mod_line = re.sub(SINGLE_LINE_COMMENT_REGEX, "", mod_line) # If it wasn't just a comment line... if len(mod_line.strip()) > 0: mod_line = re.sub(r"[^\t ].*", "", mod_line) size = len(mod_line) # debug("Indent size [" + str(size) + "]:\n" + re.sub("\n", "", line_of_text)) return size def get_completions_for_class(class_name, search_statically, local_file_lines, prefix, global_file_path_list, built_in_types, member_exclusion_regexes, show_private): # TODO: Use prefix to make suggestions. completions = [] scanned_classes = [] original_class_name_found = False function_completions = [] object_completions = [] # First, determine if it is a built in type and return those completions... # Built-in types include String, Number, etc, and are configurable in settings. for next_built_in_type in built_in_types: try: if next_built_in_type[BUILT_IN_TYPES_TYPE_ENABLED_KEY]: next_class_name = next_built_in_type[BUILT_IN_TYPES_TYPE_NAME_KEY] if next_class_name == class_name: # We are looking at a built-in type! Collect completions for it... completions = get_completions_for_built_in_type(next_built_in_type, search_statically, False, member_exclusion_regexes) original_class_name_found = True elif next_class_name == "Function" and not function_completions: function_completions = get_completions_for_built_in_type(next_built_in_type, False, True, member_exclusion_regexes) elif next_class_name == "Object" and not object_completions: object_completions = get_completions_for_built_in_type(next_built_in_type, False, True, member_exclusion_regexes) except Exception, e: print repr(e) # If we didn't find completions for a built-in type, look further... if not completions: current_class_name = class_name is_inherited = False while current_class_name and current_class_name not in scanned_classes: # print "Scanning " + current_class_name + "..." # (class_found, completions, next_class_to_scan) completion_tuple = (False, [], None) if local_file_lines: # print "Searching locally..." # Search in local file. if search_statically: completion_tuple = collect_static_completions_from_file(local_file_lines, current_class_name, is_inherited, member_exclusion_regexes, show_private) else: completion_tuple = collect_instance_completions_from_file(local_file_lines, current_class_name, is_inherited, member_exclusion_regexes, show_private) # Search globally if nothing found and not local only... if global_file_path_list and (not completion_tuple or not completion_tuple[0]): class_regex = CLASS_REGEX % re.escape(current_class_name) global_class_location_search_tuple = find_location_of_regex_in_files(class_regex, None, global_file_path_list) if global_class_location_search_tuple: # If found, perform Class method collection. file_to_open = global_class_location_search_tuple[0] class_file_lines = get_lines_for_file(file_to_open) if search_statically: completion_tuple = collect_static_completions_from_file(class_file_lines, current_class_name, is_inherited, member_exclusion_regexes, show_private) else: completion_tuple = collect_instance_completions_from_file(class_file_lines, current_class_name, is_inherited, member_exclusion_regexes, show_private) if current_class_name == class_name and completion_tuple[0]: original_class_name_found = True # print "Tuple: " + str(completion_tuple) completions.extend(completion_tuple[1]) scanned_classes.append(current_class_name) current_class_name = completion_tuple[2] is_inherited = True if original_class_name_found: # Add Object completions (if available) -- Everything is an Object completions.extend(object_completions) if search_statically: completions.extend(function_completions) # Remove all duplicates completions = list(set(completions)) # Sort completions.sort() return completions def case_insensitive_startswith(original_string, prefix): return original_string.lower().startswith(prefix.lower()) def get_completions_for_built_in_type(built_in_type, is_static, is_inherited, member_exclusion_regexes): completions = [] if is_static: static_properties = [] static_property_objs = built_in_type[BUILT_IN_TYPES_STATIC_PROPERTIES_KEY] for next_static_property_obj in static_property_objs: next_static_property = next_static_property_obj[BUILT_IN_TYPES_STATIC_PROPERTY_NAME_KEY] if not is_member_excluded(next_static_property, member_exclusion_regexes): static_properties.append(next_static_property) for next_static_property in static_properties: next_completion = get_property_completion_tuple(next_static_property, is_inherited) completions.append(next_completion) static_methods = built_in_type[BUILT_IN_TYPES_STATIC_METHODS_KEY] for next_static_method in static_methods: method_name = next_static_method[BUILT_IN_TYPES_METHOD_NAME_KEY] if not is_member_excluded(method_name, member_exclusion_regexes): method_args = [] method_insertions = [] method_args_objs = next_static_method[BUILT_IN_TYPES_METHOD_ARGS_KEY] for next_method_arg_obj in method_args_objs: method_arg = next_method_arg_obj[BUILT_IN_TYPES_METHOD_ARG_NAME_KEY] method_args.append(method_arg) method_insertion = method_arg try: method_insertion = next_method_arg_obj[BUILT_IN_TYPES_METHOD_INSERTION_KEY] except: pass method_insertions.append(method_insertion) next_completion = get_method_completion_tuple(method_name, method_args, method_insertions, is_inherited) completions.append(next_completion) else: instance_properties = [] instance_property_objs = built_in_type[BUILT_IN_TYPES_INSTANCE_PROPERTIES_KEY] for next_instance_property_obj in instance_property_objs: next_instance_property = next_instance_property_obj[BUILT_IN_TYPES_INSTANCE_PROPERTY_NAME_KEY] if not is_member_excluded(next_instance_property, member_exclusion_regexes): instance_properties.append(next_instance_property_obj[BUILT_IN_TYPES_INSTANCE_PROPERTY_NAME_KEY]) for next_instance_property in instance_properties: next_completion = get_property_completion_tuple(next_instance_property, is_inherited) completions.append(next_completion) instance_methods = built_in_type[BUILT_IN_TYPES_INSTANCE_METHODS_KEY] for next_instance_method in instance_methods: method_name = next_instance_method[BUILT_IN_TYPES_METHOD_NAME_KEY] if not is_member_excluded(method_name, member_exclusion_regexes): method_args = [] method_insertions = [] method_args_objs = next_instance_method[BUILT_IN_TYPES_METHOD_ARGS_KEY] for next_method_arg_obj in method_args_objs: method_arg = next_method_arg_obj[BUILT_IN_TYPES_METHOD_ARG_NAME_KEY] method_args.append(method_arg) method_insertion = method_arg try: method_insertion = next_method_arg_obj[BUILT_IN_TYPES_METHOD_INSERTION_KEY] except: pass method_insertions.append(method_insertion) next_completion = get_method_completion_tuple(method_name, method_args, method_insertions, is_inherited) completions.append(next_completion) return completions def collect_instance_completions_from_file(file_lines, class_name, is_inherited, member_exclusion_regexes, show_private): completions = [] extended_class = None class_found = False property_completions = [] function_completions = [] class_and_extends_regex = CLASS_REGEX_WITH_EXTENDS % class_name # Find class in file lines match_tuple = get_positions_of_regex_match_in_file(file_lines, class_and_extends_regex) if match_tuple: class_found = True row = match_tuple[0] match = match_tuple[2] extended_class = match.group(3) if extended_class: extended_class = get_class_from_end_of_chain(extended_class) # If anything is equal to this after the first line, stop looking. # At that point, the class definition has ended. indentation_size = get_indentation_size(file_lines[row]) # print str(indentation_size) + ": " + file_lines[row] # Let's dig for some info on this class! if row + 1 < len(file_lines): inside_constructor = False constructor_indentation = -1 for row_index in range(row + 1, len(file_lines)): next_row = file_lines[row_index] next_indentation = get_indentation_size(next_row) # print str(next_indentation) + ": " + next_row if next_indentation >= 0: if next_indentation > indentation_size: if inside_constructor and next_indentation <= constructor_indentation: inside_constructor = False if inside_constructor: this_assignment_regex = "([@]|(this\s*[.]))\s*([a-zA-Z0-9_$]+)\s*=" match = re.search(this_assignment_regex, next_row) if match: prop = match.group(3) if show_private or not is_member_excluded(prop, member_exclusion_regexes): prop_completion_alias = get_property_completion_alias(prop, is_inherited) prop_completion_insertion = get_property_completion_insertion(prop) prop_completion = (prop_completion_alias, prop_completion_insertion) if prop_completion not in property_completions: property_completions.append(prop_completion) else: # Not in constructor # Look for method definitions function_regex = FUNCTION_REGEX_ANY match = re.search(function_regex, next_row) if match and not re.search(STATIC_FUNCTION_REGEX, next_row): function_name = match.group(2) function_args_string = match.group(5) if show_private or not is_member_excluded(function_name, member_exclusion_regexes): if function_name != CONSTRUCTOR_KEYWORD: function_args_list = [] if function_args_string: function_args_list = function_args_string.split(",") for i in range(len(function_args_list)): # Fix each one up... next_arg = function_args_list[i] next_arg = next_arg.strip() next_arg = re.sub("[^a-zA-Z0-9_$].*", "", next_arg) function_args_list[i] = re.sub(THIS_SUGAR_SYMBOL, "", next_arg) function_alias = get_method_completion_alias(function_name, function_args_list, is_inherited) function_insertion = get_method_completion_insertion(function_name, function_args_list) function_completion = (function_alias, function_insertion) if function_completion not in function_completions: function_completions.append(function_completion) else: function_args_list = [] if function_args_string: function_args_list = function_args_string.split(",") for i in range(len(function_args_list)): # Check if it starts with @ -- this indicates an auto-set class variable next_arg = function_args_list[i] next_arg = next_arg.strip() if next_arg.startswith(THIS_SUGAR_SYMBOL): # Clean it up... next_arg = re.sub(THIS_SUGAR_SYMBOL, "", next_arg) next_arg = re.sub("[^a-zA-Z0-9_$].*", "", next_arg) if show_private or not is_member_excluded(next_arg, member_exclusion_regexes): prop_completion_alias = get_property_completion_alias(next_arg, is_inherited) prop_completion_insertion = get_property_completion_insertion(next_arg) prop_completion = (prop_completion_alias, prop_completion_insertion) if prop_completion not in property_completions: property_completions.append(prop_completion) inside_constructor = True constructor_indentation = get_indentation_size(next_row) else: # Indentation limit hit. We're not in the class anymore. break completions = property_completions + function_completions completion_tuple = (class_found, completions, extended_class) return completion_tuple def get_class_from_end_of_chain(dot_operation_chain): class_at_end = dot_operation_chain next_period_index = class_at_end.find(PERIOD_OPERATOR) while next_period_index >= 0: class_at_end = class_at_end[(next_period_index + 1):] class_at_end.strip() next_period_index = class_at_end.find(PERIOD_OPERATOR) if len(class_at_end) == 0: class_at_end = None return class_at_end def collect_static_completions_from_file(file_lines, class_name, is_inherited, member_exclusion_regexes, show_private): completions = [] extended_class = None class_found = False property_completions = [] function_completions = [] class_and_extends_regex = CLASS_REGEX_WITH_EXTENDS % class_name # Find class in file lines match_tuple = get_positions_of_regex_match_in_file(file_lines, class_and_extends_regex) if match_tuple: class_found = True row = match_tuple[0] match = match_tuple[2] extended_class = match.group(3) if extended_class: # Clean it up. next_period_index = extended_class.find(PERIOD_OPERATOR) while next_period_index >= 0: extended_class = extended_class[(next_period_index + 1):] extended_class.strip() next_period_index = extended_class.find(PERIOD_OPERATOR) if len(extended_class) == 0: extended_class = None # If anything is equal to this after the first line, stop looking. # At that point, the class definition has ended. indentation_size = get_indentation_size(file_lines[row]) # Let's dig for some info on this class! if row + 1 < len(file_lines): previous_indentation = -1 for row_index in range(row + 1, len(file_lines)): next_row = file_lines[row_index] next_indentation = get_indentation_size(next_row) # print str(next_indentation) + ": " + next_row if next_indentation >= 0: if next_indentation > indentation_size: # print "Next: " + str(next_indentation) + ", Prev: " + str(previous_indentation) # Haven't found anything yet... # Look for class-level definitions... # If current line indentation is greater than previous indentation, we're in a definition if next_indentation > previous_indentation and previous_indentation >= 0: pass # Otherwise, save this indentation and examine the current line, as it's class-level else: previous_indentation = next_indentation function_regex = STATIC_FUNCTION_REGEX match = re.search(function_regex, next_row) if match: function_name = match.group(4) if show_private or not is_member_excluded(function_name, member_exclusion_regexes): function_args_string = match.group(6) function_args_list = [] if function_args_string: function_args_list = function_args_string.split(",") for i in range(len(function_args_list)): # Fix each one up... next_arg = function_args_list[i] next_arg = next_arg.strip() next_arg = re.sub("[^a-zA-Z0-9_$].*", "", next_arg) function_args_list[i] = next_arg function_alias = get_method_completion_alias(function_name, function_args_list, is_inherited) function_insertion = get_method_completion_insertion(function_name, function_args_list) function_completion = (function_alias, function_insertion) if function_completion not in function_completions: function_completions.append(function_completion) else: # Look for static assignment assignment_regex = STATIC_ASSIGNMENT_REGEX match = re.search(assignment_regex, next_row) if match: prop = match.group(3) if show_private or not is_member_excluded(prop, member_exclusion_regexes): prop_completion_alias = get_property_completion_alias(prop, is_inherited) prop_completion_insertion = get_property_completion_insertion(prop) prop_completion = (prop_completion_alias, prop_completion_insertion) if prop_completion not in property_completions: property_completions.append(prop_completion) else: # Indentation limit hit. We're not in the class anymore. break completions = property_completions + function_completions completion_tuple = (class_found, completions, extended_class) return completion_tuple def get_property_completion_alias(property_name, is_inherited=False): indicator = PROPERTY_INDICATOR if is_inherited: indicator = INHERITED_INDICATOR + indicator completion_string = indicator + " " + property_name return completion_string def get_property_completion_insertion(property_name): completion_string = property_name completion_string = re.sub("[$]", "\$", completion_string) return completion_string def get_property_completion_tuple(property_name, is_inherited=False): completion_tuple = (get_property_completion_alias(property_name, is_inherited), get_property_completion_insertion(property_name)) return completion_tuple def get_method_completion_alias(method_name, args, is_inherited=False): indicator = METHOD_INDICATOR if is_inherited: indicator = INHERITED_INDICATOR + indicator completion_string = indicator + " " + method_name + "(" for i in range(len(args)): completion_string = completion_string + args[i] if i < len(args) - 1: completion_string = completion_string + ", " completion_string = completion_string + ")" return completion_string def get_method_completion_insertion(method_name, args): no_parens = False completion_string = re.sub("[$]", "\$", method_name) if len(args) == 1: function_match = re.search(r".*?[=\-]>.*", args[0]) if function_match: no_parens = True if no_parens: completion_string = completion_string + " " else: completion_string = completion_string + "(" for i in range(len(args)): escaped_arg = re.sub("[$]", "\$", args[i]) completion_string = completion_string + "${" + str(i + 1) + ":" + escaped_arg + "}" if i < len(args) - 1: completion_string = completion_string + ", " if not no_parens: completion_string = completion_string + ")" return completion_string def get_method_completion_tuple(method_name, arg_names, arg_insertions, is_inherited=False): completion_tuple = (get_method_completion_alias(method_name, arg_names, is_inherited), get_method_completion_insertion(method_name, arg_insertions)) return completion_tuple def get_view_contents(view): contents = "" start = 0 end = view.size() - 1 if end > start: entire_doc_region = sublime.Region(start, end) contents = view.substr(entire_doc_region) return contents def convert_file_contents_to_lines(contents): lines = contents.split("\n") count = len(lines) for i in range(count): # Don't add to the last one--that would put an extra \n if i < count - 1: lines[i] = lines[i] + "\n" return lines def get_view_content_lines(view): return convert_file_contents_to_lines(get_view_contents(view)) def is_autocomplete_trigger(text): trigger = False trigger = trigger or text == THIS_SUGAR_SYMBOL trigger = trigger or text == PERIOD_OPERATOR return trigger def is_member_excluded(member, exclusion_regexes): excluded = False for next_exclusion_regex in exclusion_regexes: if re.search(next_exclusion_regex, member): excluded = True return excluded