Files
2013-04-04 08:54:25 -04:00

236 lines
11 KiB
Python

import sublime, sublime_plugin
import re
import os
import threading
import coffee_utils
from coffee_utils import debug
from copy import copy
COFFEESCRIPT_AUTOCOMPLETE_STATUS_KEY = "coffee_autocomplete"
COFFEESCRIPT_AUTOCOMPLETE_STATUS_MESSAGE = "Coffee: Autocompleting \"%s\"..."
final_completions = []
status = {"working": False}
# TODO:
# - Type hinting using comments containing square brackets [Type] on same line or previous line
# - Codo docs searching for function parameter types
# - Better symbol parsing. Assignment lookups should consider the entire set of operands.
# X Consider all super classes (support extends)
# - Consider another feature: Override/implement methods
# - Full assignment traceback (that = this, a = b = c, knows what c is)
# - Check contents of currently open views
# - Built in types
class CoffeeAutocomplete(sublime_plugin.EventListener):
def on_query_completions(self, view, prefix, locations):
completions = copy(final_completions)
working = status["working"]
# If there is a word selection and we're looking at a coffee file...
if not completions and coffee_utils.is_coffee_syntax(view) and not working:
status["working"] = True
current_location = locations[0]
# Get the window
self.window = sublime.active_window()
# http://www.sublimetext.com/forum/viewtopic.php?f=6&t=9076
settings = sublime.load_settings(coffee_utils.SETTINGS_FILE_NAME)
built_in_types_settings = sublime.load_settings(coffee_utils.BUILT_IN_TYPES_SETTINGS_FILE_NAME)
built_in_types = built_in_types_settings.get(coffee_utils.BUILT_IN_TYPES_SETTINGS_KEY)
if not built_in_types:
built_in_types = []
custom_types_settings = sublime.load_settings(coffee_utils.CUSTOM_TYPES_SETTINGS_FILE_NAME)
custom_types = custom_types_settings.get(coffee_utils.CUSTOM_TYPES_SETTINGS_KEY)
if not custom_types:
custom_types = []
built_in_types.extend(custom_types)
# Pull the excluded dirs from preferences
excluded_dirs = settings.get(coffee_utils.PREFERENCES_COFFEE_EXCLUDED_DIRS)
if not excluded_dirs:
excluded_dirs = []
restricted_to_dirs = settings.get(coffee_utils.PREFERENCES_COFFEE_RESTRICTED_TO_PATHS)
if not restricted_to_dirs:
restricted_to_dirs = []
# List of all project folders
project_folder_list = self.window.folders()
if restricted_to_dirs:
specific_project_folders = []
for next_restricted_dir in restricted_to_dirs:
for next_project_folder in project_folder_list:
next_specific_folder = os.path.normpath(os.path.join(next_project_folder, next_restricted_dir))
specific_project_folders.append(next_specific_folder)
project_folder_list = specific_project_folders
function_return_types = settings.get(coffee_utils.FUNCTION_RETURN_TYPES_SETTINGS_KEY)
if not function_return_types:
function_return_types = []
this_aliases = settings.get(coffee_utils.PREFERENCES_THIS_ALIASES)
if not this_aliases:
this_aliases = []
member_exclusion_regexes = settings.get(coffee_utils.PREFERENCES_MEMBER_EXCLUSION_REGEXES)
if not member_exclusion_regexes:
member_exclusion_regexes = []
# Lines for the current file in view
current_file_lines = coffee_utils.get_view_content_lines(view)
# TODO: Smarter previous word selection
preceding_symbol = coffee_utils.get_preceding_symbol(view, prefix, locations)
immediately_preceding_symbol = coffee_utils.get_preceding_symbol(view, "", locations)
preceding_function_call = coffee_utils.get_preceding_function_call(view).strip()
# Determine preceding token, if any (if a period was typed).
token = coffee_utils.get_preceding_token(view).strip()
# TODO: Smarter region location
symbol_region = sublime.Region(locations[0] - len(prefix), locations[0] - len(prefix))
if (preceding_function_call or token or coffee_utils.THIS_SUGAR_SYMBOL == preceding_symbol) and coffee_utils.is_autocomplete_trigger(immediately_preceding_symbol):
self.window.active_view().run_command('hide_auto_complete')
thread = CoffeeAutocompleteThread(project_folder_list, excluded_dirs, this_aliases, current_file_lines, preceding_symbol, prefix, preceding_function_call, function_return_types, token, symbol_region, built_in_types, member_exclusion_regexes)
thread.start()
self.check_operation(thread, final_completions, current_location, token, status)
else:
status["working"] = False
elif completions:
self.clear_completions(final_completions)
return completions
def check_operation(self, thread, final_completions, current_location, token, status, previous_progress_indicator_tuple=None):
if not thread.is_alive():
if thread.completions:
final_completions.extend(thread.completions)
# Hide the default auto-complete and show ours
self.window.active_view().run_command('hide_auto_complete')
sublime.set_timeout(lambda: self.window.active_view().run_command('auto_complete'), 1)
self.window.active_view().erase_status(COFFEESCRIPT_AUTOCOMPLETE_STATUS_KEY)
status["working"] = False
else:
token = thread.token
# Create the command's goto definition text, including the selected word. For the status bar.
status_text = COFFEESCRIPT_AUTOCOMPLETE_STATUS_MESSAGE % token
# Get a tuple containing the progress text, progress position, and progress direction.
# This is used to animate a progress indicator in the status bar.
current_progress_indicator_tuple = coffee_utils.get_progress_indicator_tuple(previous_progress_indicator_tuple)
# Get the progress text
progress_indicator_status_text = current_progress_indicator_tuple[0]
# Set the status bar text so the user knows what's going on
self.window.active_view().set_status(COFFEESCRIPT_AUTOCOMPLETE_STATUS_KEY, status_text + " " + progress_indicator_status_text)
# Check again momentarily to see if the operation has completed.
sublime.set_timeout(lambda: self.check_operation(thread, final_completions, current_location, token, status, current_progress_indicator_tuple), 100)
def clear_completions(self, final_completions):
debug("Clearing completions...")
while len(final_completions) > 0:
final_completions.pop()
class CoffeeAutocompleteThread(threading.Thread):
def __init__(self, project_folder_list, excluded_dirs, this_aliases, current_file_lines, preceding_symbol, prefix, preceding_function_call, function_return_types, token, symbol_region, built_in_types, member_exclusion_regexes):
self.project_folder_list = project_folder_list
self.excluded_dirs = excluded_dirs
self.this_aliases = this_aliases
self.current_file_lines = current_file_lines
self.preceding_symbol = preceding_symbol
self.prefix = prefix
self.preceding_function_call = preceding_function_call
self.function_return_types = function_return_types
self.token = token
self.symbol_region = symbol_region
self.built_in_types = built_in_types
self.member_exclusion_regexes = member_exclusion_regexes
# None if no completions found, or an array of the completion tuples
self.completions = None
threading.Thread.__init__(self)
def run(self):
project_folder_list = self.project_folder_list
excluded_dirs = self.excluded_dirs
this_aliases = self.this_aliases
current_file_lines = self.current_file_lines
preceding_symbol = self.preceding_symbol
prefix = self.prefix
preceding_function_call = self.preceding_function_call
function_return_types = self.function_return_types
token = self.token
symbol_region = self.symbol_region
built_in_types = self.built_in_types
member_exclusion_regexes = self.member_exclusion_regexes
selected_word = token[token.rfind(".") + 1:]
completions = []
# First see if it is a special function return definition, like $ for $("#selector")
if preceding_function_call:
for next_return_type in function_return_types:
function_names = next_return_type[coffee_utils.FUNCTION_RETURN_TYPE_FUNCTION_NAMES_KEY]
if preceding_function_call in function_names:
return_type = next_return_type[coffee_utils.FUNCTION_RETURN_TYPE_TYPE_NAME_KEY]
completions = coffee_utils.get_completions_for_class(return_type, False, None, prefix, None, built_in_types, member_exclusion_regexes, False)
if not completions:
# Prepare to search globally if we need to...
# Coffeescript filename regex
coffeescript_filename_regex = coffee_utils.COFFEE_FILENAME_REGEX
# All coffeescript file paths
all_coffee_file_paths = coffee_utils.get_files_in(project_folder_list, coffeescript_filename_regex, excluded_dirs)
# If @ typed, process as "this."
if preceding_symbol == coffee_utils.THIS_SUGAR_SYMBOL:
# Process as "this."
this_type = coffee_utils.get_this_type(current_file_lines, symbol_region)
if this_type:
completions = coffee_utils.get_completions_for_class(this_type, False, current_file_lines, prefix, all_coffee_file_paths, built_in_types, member_exclusion_regexes, True)
pass
elif preceding_symbol == coffee_utils.PERIOD_OPERATOR:
# If "this" or a substitute for it, process as "this."
if selected_word == coffee_utils.THIS_KEYWORD or selected_word in this_aliases:
# Process as "this."
this_type = coffee_utils.get_this_type(current_file_lines, symbol_region)
if this_type:
completions = coffee_utils.get_completions_for_class(this_type, False, current_file_lines, prefix, all_coffee_file_paths, built_in_types, member_exclusion_regexes, True)
else:
# If TitleCase, assume a class, and that we want static properties and functions.
if coffee_utils.is_capitalized(selected_word):
# Assume it is either in the current view or in a coffee file somewhere
completions = coffee_utils.get_completions_for_class(selected_word, True, current_file_lines, prefix, all_coffee_file_paths, built_in_types, member_exclusion_regexes, False)
if not completions:
# Now we search globally...
completions = coffee_utils.get_completions_for_class(selected_word, True, None, prefix, all_coffee_file_paths, built_in_types, member_exclusion_regexes, False)
# If nothing yet, assume a variable.
if not completions:
variable_type = coffee_utils.get_variable_type(current_file_lines, token, symbol_region, all_coffee_file_paths, built_in_types, [])
if variable_type:
# Assume it is either in the current view or in a coffee file somewhere
completions = coffee_utils.get_completions_for_class(variable_type, False, current_file_lines, prefix, all_coffee_file_paths, built_in_types, member_exclusion_regexes, False)
if not completions:
# Now we search globally for a class... Maybe they're making a static call on something lowercase? Bad design, but check anyways.
completions = coffee_utils.get_completions_for_class(selected_word, True, None, prefix, all_coffee_file_paths, built_in_types, member_exclusion_regexes, False)
if completions:
self.completions = completions