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 |