236 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			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 |