250 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			250 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import sublime, sublime_plugin
 | |
| import re
 | |
| import os
 | |
| import threading
 | |
| import coffee_utils
 | |
| from coffee_utils import debug
 | |
| 
 | |
| COMMAND_NAME = 'coffee_goto_definition'
 | |
| STATUS_MESSAGE_DEFINITION_FOUND = "Coffee: Definition for \"%s\" found."
 | |
| STATUS_MESSAGE_NO_DEFINITION_FOUND = "Coffee: No definition for \"%s\" found."
 | |
| STATUS_MESSAGE_COFFEE_GOTO_DEFINITION = "Coffee: Goto Definition of \"%s\""
 | |
| 
 | |
| # SEARCH ORDER:
 | |
| # Current file class (TitleCaps only)
 | |
| # Current file function
 | |
| # Current file assignment
 | |
| # Global TitleCaps.coffee class
 | |
| # Global search for class (TitleCaps only)
 | |
| # Global search for function
 | |
| 
 | |
| # TODO:
 | |
| # X Add config for "this" aliases (DONE)
 | |
| # - Codo docs searching for function parameter types
 | |
| # X Goto definition knows about function parameters and for loop variables
 | |
| # - Smarter operand parsing. E.g. Given: this.test = "test", when goto "test", look for "this.test = ", not "test ="
 | |
| # - Check contents of currently open views
 | |
| # - Menu integration
 | |
| 
 | |
| class CoffeeGotoDefinitionCommand(sublime_plugin.TextCommand):
 | |
| 	def run(self, edit):
 | |
| 
 | |
| 		# Get the window
 | |
| 		self.window = sublime.active_window()
 | |
| 
 | |
| 		# The current view
 | |
| 		view = self.view
 | |
| 		# Lines for currently viewed file
 | |
| 		current_file_lines = coffee_utils.get_view_content_lines(view)
 | |
| 		
 | |
| 		# Get currently selected word
 | |
| 		coffee_utils.select_current_word(view)
 | |
| 		selected_word = coffee_utils.get_selected_word(view)
 | |
| 
 | |
| 		selected_region = self.view.sel()[0]
 | |
| 
 | |
| 		# http://www.sublimetext.com/forum/viewtopic.php?f=6&t=9076
 | |
| 		settings = sublime.load_settings(coffee_utils.SETTINGS_FILE_NAME)
 | |
| 
 | |
| 		# 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
 | |
| 
 | |
| 		# If there is a word selection and we're looking at a coffee file...
 | |
| 		if len(selected_word) > 0 and coffee_utils.is_coffee_syntax(view):
 | |
| 
 | |
| 			thread = CoffeeGotoDefinitionThread(project_folder_list, current_file_lines, selected_word, excluded_dirs, selected_region)
 | |
| 			thread.start()
 | |
| 			self.check_operation(thread)
 | |
| 
 | |
| 	def check_operation(self, thread, previous_progress_indicator_tuple=None):
 | |
| 		selected_word = thread.selected_word
 | |
| 		if not thread.is_alive():
 | |
| 
 | |
| 			# Flatten any selection ranges
 | |
| 			if len(self.view.sel()) > 0:
 | |
| 				region = self.view.sel()[0]
 | |
| 				debug(region)
 | |
| 				end_point = region.end()
 | |
| 				region_to_select = sublime.Region(end_point, end_point)
 | |
| 				coffee_utils.select_region_in_view(self.view, region_to_select)
 | |
| 
 | |
| 			matched_location_tuple = thread.matched_location_tuple
 | |
| 			if matched_location_tuple:
 | |
| 				# debug("Match found!")
 | |
| 				file_to_open = matched_location_tuple[0]
 | |
| 				row = matched_location_tuple[1] + 1
 | |
| 				column = matched_location_tuple[2] + 1
 | |
| 				match = matched_location_tuple[3]
 | |
| 				row_start_index = matched_location_tuple[4]
 | |
| 				# If there is a file to open...
 | |
| 				if file_to_open:
 | |
| 					# Open the file in the editor
 | |
| 					coffee_utils.open_file_at_position(self.window, file_to_open, row, column)
 | |
| 				# Otherwise, assume we found the match in the current view
 | |
| 				else:
 | |
| 					match_end = row_start_index + match.start() + len(match.group())
 | |
| 					region_to_select = sublime.Region(match_end, match_end)
 | |
| 					coffee_utils.select_region_in_view(self.view, region_to_select)
 | |
| 					self.view.show(region_to_select)
 | |
| 
 | |
| 				self.window.active_view().set_status(COMMAND_NAME, STATUS_MESSAGE_DEFINITION_FOUND % selected_word)
 | |
| 			else:
 | |
| 				self.window.active_view().set_status(COMMAND_NAME, STATUS_MESSAGE_NO_DEFINITION_FOUND % selected_word)
 | |
| 
 | |
| 		else:
 | |
| 			# Create the command's goto definition text, including the selected word. For the status bar.
 | |
| 			goto_definition_status_text = STATUS_MESSAGE_COFFEE_GOTO_DEFINITION % selected_word
 | |
| 			# 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(COMMAND_NAME, goto_definition_status_text + " " + progress_indicator_status_text)
 | |
| 			# Check again momentarily to see if the operation has completed.
 | |
| 			sublime.set_timeout(lambda: self.check_operation(thread, current_progress_indicator_tuple), 100)
 | |
| 
 | |
| class CoffeeGotoDefinitionThread(threading.Thread):
 | |
| 	
 | |
| 	def __init__(self, project_folder_list, current_file_lines, selected_word, excluded_dirs, selected_region):
 | |
| 		self.project_folder_list = project_folder_list
 | |
| 		self.current_file_lines = current_file_lines
 | |
| 		self.selected_word = selected_word
 | |
| 		self.excluded_dirs = excluded_dirs
 | |
| 		self.selected_region = selected_region
 | |
| 		# None if no match was found, or a tuple containing the filename, row, column and match
 | |
| 		self.matched_location_tuple = None
 | |
| 		threading.Thread.__init__(self)
 | |
| 
 | |
| 	def run(self):
 | |
| 
 | |
| 		project_folder_list = self.project_folder_list
 | |
| 		current_file_lines = self.current_file_lines
 | |
| 		selected_word = self.selected_word
 | |
| 		excluded_dirs = self.excluded_dirs
 | |
| 		selected_region = self.selected_region
 | |
| 
 | |
| 		# This will be assigned whem a match is made
 | |
| 		matched_location_tuple = None
 | |
| 
 | |
| 		# The regular expression used to search for the selected class
 | |
| 		class_regex = coffee_utils.CLASS_REGEX % re.escape(selected_word)
 | |
| 		# The regex used to search for the selected function
 | |
| 		function_regex = coffee_utils.FUNCTION_REGEX % re.escape(selected_word)
 | |
| 		# The regex used to search for the selected variable assignment
 | |
| 		assignment_regex = coffee_utils.ASSIGNMENT_REGEX % re.escape(selected_word)
 | |
| 		# The regex used to search for the selected variable as a parameter in a method
 | |
| 		param_regex = coffee_utils.PARAM_REGEX.format(name=re.escape(selected_word))
 | |
| 
 | |
| 		# The regex used to search for the selected variable as a for loop var
 | |
| 		for_loop_regex = coffee_utils.FOR_LOOP_REGEX % re.escape(selected_word)
 | |
| 
 | |
| 		debug(("Selected: \"%s\"" % selected_word))
 | |
| 
 | |
| 		# ------ CURRENT FILE: CLASS (TitleCaps ONLY) ------------
 | |
| 
 | |
| 		if not matched_location_tuple:
 | |
| 
 | |
| 				# If so, we assume it is a class. 
 | |
| 				debug("Checking for local class %s..." % selected_word)
 | |
| 				class_location_search_tuple = coffee_utils.find_location_of_regex_in_files(class_regex, current_file_lines, [])
 | |
| 				if class_location_search_tuple:
 | |
| 					matched_location_tuple = class_location_search_tuple
 | |
| 
 | |
| 		# ------ GLOBAL SEARCH: CLASS ----------------------------
 | |
| 
 | |
| 		if not matched_location_tuple:
 | |
| 
 | |
| 			# 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)
 | |
| 
 | |
| 			debug("Checking globally for class %s..." % selected_word)
 | |
| 			# Assume it is a file called selected_word.coffee
 | |
| 			exact_file_name_regex = "^" + re.escape(selected_word) + coffee_utils.COFFEE_EXTENSION_WITH_DOT + "$"
 | |
| 			exact_name_file_paths = coffee_utils.get_files_in(project_folder_list, exact_file_name_regex, excluded_dirs)
 | |
| 			exact_location_search_tuple = coffee_utils.find_location_of_regex_in_files(class_regex, None, exact_name_file_paths)
 | |
| 			if exact_location_search_tuple:
 | |
| 				matched_location_tuple = exact_location_search_tuple
 | |
| 			else:
 | |
| 				global_class_location_search_tuple = coffee_utils.find_location_of_regex_in_files(class_regex, None, all_coffee_file_paths)
 | |
| 				if global_class_location_search_tuple:
 | |
| 					matched_location_tuple = global_class_location_search_tuple
 | |
| 
 | |
| 		# ------ CURRENT FILE: FUNCTION --------------------------
 | |
| 		if not matched_location_tuple:
 | |
| 			debug("Checking for local function %s..." % selected_word)
 | |
| 			local_function_location_search_tuple = coffee_utils.find_location_of_regex_in_files(function_regex, current_file_lines, [])
 | |
| 			if local_function_location_search_tuple:
 | |
| 				matched_location_tuple = local_function_location_search_tuple
 | |
| 
 | |
| 		# ------ CURRENT FILE: ASSIGNMENT ------------------------
 | |
| 
 | |
| 		if not matched_location_tuple:
 | |
| 			
 | |
| 			debug("Checking for local assignment of %s..." % selected_word)
 | |
| 			backwards_match_tuple = coffee_utils.search_backwards_for(current_file_lines, assignment_regex, selected_region)
 | |
| 			if backwards_match_tuple:
 | |
| 				filename_tuple = tuple([None])
 | |
| 				matched_location_tuple = filename_tuple + backwards_match_tuple
 | |
| 			else:
 | |
| 				# Nothing found. Now let's look backwards for a method parameter
 | |
| 				param_match_tuple = coffee_utils.search_backwards_for(current_file_lines, param_regex, selected_region)
 | |
| 				if param_match_tuple:
 | |
| 					filename_tuple = tuple([None])
 | |
| 					matched_location_tuple = filename_tuple + param_match_tuple	
 | |
| 				else:
 | |
| 					for_loop_match_tuple = coffee_utils.search_backwards_for(current_file_lines, for_loop_regex, selected_region)
 | |
| 					if for_loop_match_tuple:
 | |
| 						filename_tuple = tuple([None])
 | |
| 						matched_location_tuple = filename_tuple + for_loop_match_tuple
 | |
| 					# Otherwise, forwards search for it. It could be defined in the constructor.
 | |
| 					else:
 | |
| 						forwards_match_tuple = coffee_utils.find_location_of_regex_in_files(assignment_regex, current_file_lines, [])
 | |
| 						if forwards_match_tuple:
 | |
| 							matched_location_tuple = forwards_match_tuple
 | |
| 
 | |
| 		# ------ GLOBAL SEARCH: FUNCTION -------------------------
 | |
| 
 | |
| 		if not matched_location_tuple:
 | |
| 
 | |
| 			# 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)
 | |
| 
 | |
| 			debug("Checking globally for function %s..." % selected_word)
 | |
| 			global_function_location_search_tuple = coffee_utils.find_location_of_regex_in_files(function_regex, None, all_coffee_file_paths)
 | |
| 			if global_function_location_search_tuple:
 | |
| 				matched_location_tuple = global_function_location_search_tuple
 | |
| 
 | |
| 		# ------ DOT OPERATION LOOKUP (TBD) ----------------------
 | |
| 		# TODO: Pull out dot operator object, determine its assignment type, find class, goto method/property.
 | |
| 		#	    Also, determine where to put this lookup.
 | |
| 
 | |
| 		# ------ SUPER METHOD LOOKUP (TBD) -----------------------
 | |
| 		# TODO: If selected_word is "super", assume a function and then attempt to find 
 | |
| 		#       extending class and open it to the function the cursor is within.
 | |
| 
 | |
| 		# ------ STORE MATCH RESULTS -----------------------------
 | |
| 		# If not None, then we found something that matched the search!
 | |
| 		if matched_location_tuple:
 | |
| 			self.matched_location_tuple = matched_location_tuple |