def evaluate_settings(self): # validate final project directory (by settings) project_directory = os.path.join(self.directory, self.get_setting("PROJECT_DIRECTORY")) if os.path.exists(project_directory): self.project_directory = Path.posix(project_directory) else: self.project_directory = Path.posix(self.directory) warn(ID, "project directory in settings in not a valid folder") # setup project cache triggers = self.settings.get("scopes", self.get_setting("TRIGGER")) valid_file_extensions = get_valid_extensions(triggers) folders_to_exclude = self.get_setting("EXCLUDE_FOLDERS") self.filecache = FileCache(valid_file_extensions, folders_to_exclude, self.project_directory) # evaluate base directory self.base_directory = Validate.sanitize_base_directory( self.settings.get("BASE_DIRECTORY", ""), self.project_directory, self.directory ) verbose(ID, "new project created", self.project_directory) verbose(ID, "Base directory at", "'" + self.base_directory + "'")
def update(): """ call me anytime a new view has gained focus. This includes activation of a new window, which should have an active view """ global valid, is_enabled if not is_enabled: return False temp = False window = sublime.active_window() if window is None: logger.log(ID, "Abort -- no active window") valid = False return valid view = window.active_view() if view is None: logger.log(ID, "Abort -- no active view") valid = False return valid file = Path.posix(view.file_name()) if not file: logger.log(ID, "Abort -- view has not yet been saved to file") temp = True return valid if state.get("file") == file: logger.log(ID, "Abort -- view already updated") return valid folders = list(map(lambda f: Path.posix(f), window.folders())) project_folder = get_closest_folder(file, folders) if project_folder is False: logger.log(ID, "Abort -- file not part of a project (folder)") valid = False return valid # notify settings of new project folder if state.get("project_folder") != project_folder: settings.update_project_settings() settings.update_project_folder_settings(project_folder) valid = True # @TODO cache state["file"] = file state["directory"] = sanitize_directory(file, project_folder) state["folders"] = folders state["project_folder"] = project_folder state["cache"] = get_file_cache(project_folder) logger.start_block() logger.verbose(ID, "Updated", state) return valid
def get_file_cache(folder): if not folder in file_caches: valid_file_extensions = get_valid_extensions(settings.get("trigger")) logger.verbose(ID, "Build cache for " + folder + " (", valid_file_extensions, ") excluding", settings.get("exclude_folders")) file_caches[folder] = FileCache(valid_file_extensions, settings.get("exclude_folders"), folder) return file_caches.get(folder)
def file_is_cached(self, file_name): """ returns False if the given file is not within cache tests files with full path or relative from project directory """ name, extension = os.path.splitext(file_name) extension = extension[1:] if not extension in self.valid_extensions: verbose(ID_CACHE, "file to cache has no valid extension", extension) return True file_name = re.sub(self.directory, "", file_name) return self.cache.get(file_name, False) is not False
def find_trigger(current_scope, expression, byCommand=False): """ Returns the first trigger matching the given scope and expression """ triggers = settings.get("TRIGGER") if not byCommand: # get any triggers that match the requirements and may start automatically triggers = get_matching_autotriggers(current_scope, settings.get("TRIGGER")) if not bool(triggers): verbose(ID, "abort query, no valid scope-regex for current context") return False # check if one of the triggers match the current context (expression, scope) return Context.find_trigger(expression, current_scope, triggers)
def activate_project(window): if ProjectManager.active: # fetch project ProjectManager.current_project = ProjectManager.get_project(window) CurrentFile.evaluate_current(window.active_view(), ProjectManager.get_current_project()) if ProjectManager.has_current_project(): # update project settings project_settings = Settings.project(window) ProjectManager.get_current_project().update_settings(ProjectManager.ffp_settings, project_settings) verbose(ID, "activate project", ProjectManager.get_current_project().get_directory()) else: verbose(ID, "this is not a project")
def start_tracking(self, view, command_name=None): self.track_insert["active"] = True self.track_insert["start_line"] = Selection.get_line(view) """ - sublime inserts completions by replacing the current word - this results in wrong path insertions if the query contains word_separators like slashes - thus the path until current word has to be removed after insertion - ... and possibly afterwards """ context = Context.get_context(view) needle = context.get("needle") word = re.escape(Selection.get_word(view)) self.post_remove = re.sub(word + "$", "", needle) verbose(ID, "start tracking", self.post_remove)
def search_completions(self, needle, project_folder, valid_extensions, base_path=False): """ retrieves a list of valid completions, containing fuzzy searched needle Parameters ---------- needle : string -- to search in files project_folder : string -- folder to search in, cached via add valid_extensions : array -- list of valid file extensions base_path : string -- of current file, creates a relative path if not False with_extension : boolean -- insert extension return : List -- containing sublime completions """ project_files = self.cache.files if (project_files is None): return False # basic: strip any dots needle = re.sub("\.\./", "", needle) needle = re.sub("\.\/", "", needle) # remove starting slash needle = re.sub("^\/", "", needle) # cleanup needle = re.sub('["\'\(\)$]', '', needle) # prepare for regex extension string needle = re.escape(needle); # build search expression regex = ".*" for i in needle: regex += i + ".*" verbose(ID, "scan", len(project_files), "files for", needle, valid_extensions); # get matching files result = [] for filepath in project_files: properties = project_files.get(filepath) """ properties[0] = escaped filename without extension, like "test/mock/project/index" properties[1] = file extension, like "html" properties[2] = file displayed as suggestion, like 'test/mock/project/index html' """ if ((properties[1] in valid_extensions or "*" in valid_extensions) and re.match(regex, filepath, re.IGNORECASE)): completion = self.get_completion(filepath, properties[2], base_path) result.append(completion) return (result, sublime.INHIBIT_EXPLICIT_COMPLETIONS | sublime.INHIBIT_WORD_COMPLETIONS)
def on_query_completions(self, view, prefix, locations): if config["DISABLE_AUTOCOMPLETION"] and not Query.by_command(): return False if self.track_insert["active"] is False: self.start_tracking(view) if CurrentFile.is_valid(): verbose(ID, "-> query completions") completions = FuzzyFilePath.on_query_completions(view, CurrentFile.get_project_directory(), CurrentFile.get_directory()) if completions is not False: return completions self.finish_tracking(view) return False
def get_filepaths(view, query): global start_expression trigger = resolve_trigger(view, query) log(ID_TRIGGER, trigger) if query.build(start_expression.get("needle"), trigger, current_state.get_directory()) is False: # query is valid, but may not be triggered: not forced, no auto-options verbose(ID, "abort - no auto trigger found") return False # remembed the path for `update_inserted_filepath`, query will be reset... state["base_directory"] = query.get_post_remove_path() return current_state.search_completions( query.get_needle(), current_state.get_project_directory(), query.get_extensions(), query.get_base_path())
def rebuild_filecache(folder=None): if not folder: if state.get("cache"): logger.verbose( ID, "rebuild current filecache of folder " + state.get("project_folder")) state.get("cache").rebuild() return folder = Path.posix(folder) if not folder in file_caches: logger.log( ID, "Abort rebuild filecache -- folder " + folder + " not cached") return False logger.verbose(ID, "rebuild current filecache of folder " + folder) file_caches.get(folder).rebuild()
def resolve_trigger(view, query): global start_expression # parse current context, may contain 'is_valid: False' start_expression = Context.get_context(view) if start_expression["error"] and not query.by_command(): verbose(ID, "abort - not a valid context") return False current_scope = Selection.get_scope(view) trigger = find_trigger(current_scope, start_expression, query.by_command()) # currently trigger is required in Query.build if trigger is False: verbose(ID, "abort - no trigger found") return False return trigger
def read(self, folder, base=None): """return all files in folder""" folder_cache = {} base = base if base is not None else folder # test ignore expressions on current path for test in self.exclude_folders: if re.search(test, folder) is not None: verbose(ID, "skip " + folder) return folder_cache # ressources = for ressource in os.listdir(folder): current_path = os.path.join(folder, ressource) if (os.path.isfile(current_path)): relative_path = os.path.relpath(current_path, base) filename, extension = os.path.splitext(relative_path) extension = extension[1:] # posix required for windows, else absolute paths are wrong: /asd\ads\ relative_path = posix(relative_path) # substitute $ which prevents errors in further processing. is replaced again in completion.py post repl relative_path = re.sub("\$", settings.get("escape_dollar"), relative_path) if extension in self.extensions: current_filename = posix(filename) folder_cache[relative_path] = [ # modified filepath. $ hack is reversed in post_commit_completion re.sub("\$", settings.get("escape_dollar"), current_filename), # extension of file extension, # sublime completion text current_filename + "\t" + extension ] elif (not ressource.startswith('.') and os.path.isdir(current_path)): folder_cache.update(self.read(current_path, base)) return folder_cache
def get_filepath_completions(view): if not state.is_valid(): Query.reset() return False verbose(ID, "get filepath completions") completions = Completion.get_filepaths(view, Query) if completions and len(completions[0]) > 0: Completion.start(Query.get_replacements()) view.run_command('_enter_insert_mode') # vintageous log("{0} completions found".format(len(completions))) else: if Query.get_needle() is not None: sublime.status_message("FFP no filepaths found for '" + Query.get_needle() + "'") Completion.stop() Query.reset() return completions
def read(self, folder, base=None): """return all files in folder""" folder_cache = {} base = base if base is not None else folder # test ignore expressions on current path for test in self.exclude_folders: if re.search(test, folder) is not None: verbose(ID, "skip " + folder) return folder_cache # ressources = for ressource in os.listdir(folder): current_path = os.path.join(folder, ressource) if (os.path.isfile(current_path)): relative_path = os.path.relpath(current_path, base) filename, extension = os.path.splitext(relative_path) extension = extension[1:] # posix required for windows, else absolute paths are wrong: /asd\ads\ relative_path = re.sub("\$", config["ESCAPE_DOLLAR"], posix(relative_path)) if extension in self.extensions: folder_cache[relative_path] = [ # modified filepath. $ hack is reversed in post_commit_completion re.sub("\$", config["ESCAPE_DOLLAR"], posix(filename)), # extension of file extension, # sublime completion text posix(filename) + "\t" + extension ] elif (not ressource.startswith('.') and os.path.isdir(current_path)): folder_cache.update(self.read(current_path, base)) return folder_cache
def evaluate_current(view, project): cache = CurrentFile.cache.get(view.id()) if cache: verbose(ID, "file cached", cache) CurrentFile.current = cache return cache if not project: # not a project verbose(ID, "no project set") CurrentFile.current = CurrentFile.default return file_name = Path.posix(view.file_name()) if not file_name: # not saved on disk CurrentFile.current = get_default() CurrentFile.current["is_temp"] = True CurrentFile.cache[view.id()] = CurrentFile.current verbose(ID, "file not saved") return project_directory = project.get_directory() if project_directory not in file_name: # not within project CurrentFile.current = CurrentFile.default verbose(ID, "file not within a project") return # add current view to cache CurrentFile.current = get_default() CurrentFile.current["project_directory"] = project_directory CurrentFile.current["directory"] = re.sub(project_directory, "", file_name) CurrentFile.current["directory"] = re.sub("^[\\\\/\.]*", "", CurrentFile.current["directory"]) CurrentFile.current["directory"] = os.path.dirname(CurrentFile.current["directory"]) verbose(ID, "File cached", file_name) CurrentFile.cache[view.id()] = CurrentFile.current
def abort_tracking(self): self.track_insert["active"] = False verbose(ID, "abort tracking")
def on_project_activated(window): """ a new project has received focus """ verbose(ID, "activate project") state.update()
def run(self): verbose(ID, "START adding files in", self.folder) self.files = self.read(self.folder) verbose(ID, len(self.files), "files cached")
def on_post_save_async(self, view): if CurrentFile.is_temp(): verbose(ID, "temp file saved, reevaluate") CurrentFile.cache[view.id()] = None ProjectManager.rebuild_filecache() self.on_activated(view)
def on_post_insert_completion(self, view, command_name): if FuzzyFilePath.completion_active(): verbose(ID, "-> post insert completion") FuzzyFilePath.on_post_insert_completion(view, self.post_remove) FuzzyFilePath.completion_stop()
def run(self): verbose(ID, "START adding files in", self.folder) self.files = self.read(self.folder) #verbose(ID, len(self.files), "files cached") print("FuzzyFilePath cached {0} files in {1}".format( len(self.files), self.folder))
def get_context(view): error = False valid = True valid_needle = True position = Selection.get_position(view) # regions word_region = view.word(position) line_region = view.line(position) pre_region = sublime.Region(line_region.a, word_region.a) post_region = sublime.Region(word_region.b, line_region.b) # text line = view.substr(line_region) word = view.substr(word_region) pre = view.substr(pre_region) post = view.substr(post_region) error = re.search("[" + NEEDLE_INVALID_CHARACTERS + "]", word) needle_region = view.word(position) # grab everything in 'separators' needle = "" separator = False pre_match = "" # search for a separator before current word, i.e. <">path/to/<position> pre_quotes = re.search( "([" + NEEDLE_SEPARATOR_BEFORE + "])([^" + NEEDLE_SEPARATOR + "]*)$", pre) if pre_quotes: needle += pre_quotes.group(2) + word separator = pre_quotes.group(1) pre_match = pre_quotes.group(2) needle_region.a -= len(pre_quotes.group(2)) else: # use whitespace as separator pre_quotes = re.search("(\s)([^" + NEEDLE_SEPARATOR + "\s]*)$", pre) if pre_quotes: needle = pre_quotes.group(2) + word separator = pre_quotes.group(1) pre_match = pre_quotes.group(2) needle_region.a -= len(pre_quotes.group(2)) if pre_quotes: post_quotes = re.search("^([" + NEEDLE_SEPARATOR_AFTER + "]*)", post) if post_quotes: needle += post_quotes.group(1) needle_region.b += len(post_quotes.group(1)) else: logger.verbose(ID, "no post quotes found => invalid") valid = False elif not re.search("[" + NEEDLE_INVALID_CHARACTERS + "]", needle): needle = pre + word needle_region.a = pre_region.a else: needle = word # grab prefix prefix_region = sublime.Region(line_region.a, pre_region.b - len(pre_match) - 1) prefix_line = view.substr(prefix_region) # # print("prefix line", prefix_line) #define? (["...", "..."]) -> before? # before: ABC =:([ prefix = re.search( "\s*([" + NEEDLE_CHARACTERS + "]+)[" + DELIMITER + "]*$", prefix_line) if prefix is None: # validate array, like define(["...", ".CURSOR."]) prefix = re.search( "^\s*([" + NEEDLE_CHARACTERS + "]+)[" + DELIMITER + "]+", prefix_line) if prefix: # print("prefix:", prefix.group(1)) prefix = prefix.group(1) tag = re.search("<\s*([" + NEEDLE_CHARACTERS + "]*)\s*[^>]*$", prefix_line) if tag: tag = tag.group(1) # print("tag:", tag) propertyName = re.search( "[\s\"\'']*([" + NEEDLE_CHARACTERS + "]*)[\s\"\']*\:[^\:]*$", prefix_line) if propertyName: propertyName = propertyName.group(1) # print("style:", style) if separator is False: logger.verbose(ID, "separator undefined => invalid", needle) valid_needle = False valid = False elif re.search("[" + NEEDLE_INVALID_CHARACTERS + "]", needle): logger.verbose(ID, "invalid characters in needle => invalid", needle) valid_needle = False valid = False elif prefix is None and separator.strip() == "": logger.verbose(ID, "prefix undefined => invalid", needle) valid = False return { "is_valid": valid, "valid_needle": valid_needle, "needle": needle, "prefix": prefix, "tagName": tag, "style": propertyName, "region": needle_region, "word": word, # really do not use any of this "error": error }
def on_query_completion_inserted(view, post_remove): if Completion.is_active(): verbose(ID, "query completion inserted") Completion.update_inserted_filepath(view, post_remove) Completion.stop()
def on_query_completions(view, project_folder, current_folder): global Context, Selection current_scope = Selection.get_scope(view) if not Query.by_command(): triggers = get_matching_autotriggers(current_scope, config["TRIGGER"]) else: triggers = config["TRIGGER"] if not bool(triggers): verbose(ID, "abort query, no valid scope-regex for current context") return False # parse current context, may contain 'is_valid: False' expression = Context.get_context(view) if expression["error"] and not Query.by_command(): verbose(ID, "abort not a valid context") return False # check if there is a trigger for the current expression trigger = Context.find_trigger(expression, current_scope, triggers) # verbose("trigger", trigger) # expression | trigger | force | ACTION | CURRENT # -----------|----------|-------|-------------------|-------- # invalid | False | False | abort | abort # invalid | False | True | query needle | abort # invalid | True | False | query | # invalid | True | True | query +override | # valid | False | False | abort | abort # valid | False | True | query needle | abort # valid | True | False | query | # valid | True | True | query +override | # currently trigger is required in Query.build if trigger is False: verbose(ID, "abort completion, no trigger found") return False if not expression["valid_needle"]: word = Selection.get_word(view) expression["needle"] = re.sub("[^\.A-Za-z0-9\-\_$]", "", word) verbose(ID, "changed invalid needle to {0}".format(expression["needle"])) else: verbose(ID, "context evaluation {0}".format(expression)) if Query.build(expression.get("needle"), trigger, current_folder, project_folder) is False: # query is valid, but may not be triggered: not forced, no auto-options verbose(ID, "abort valid query: auto trigger disabled") return False verbose(ID, ".───────────────────────────────────────────────────────────────") verbose(ID, "| scope settings: {0}".format(trigger)) verbose(ID, "| search needle: '{0}'".format(Query.needle)) verbose(ID, "| in base path: '{0}'".format(Query.base_path)) FuzzyFilePath.start_expression = expression completions = ProjectManager.search_completions(Query.needle, project_folder, Query.extensions, Query.base_path) if completions and len(completions[0]) > 0: Completion.start(Query.replace_on_insert) view.run_command('_enter_insert_mode') # vintageous log("| => {0} completions found".format(len(completions))) else: sublime.status_message("FFP no filepaths found for '" + Query.needle + "'") Completion.stop() log("| => no valid files found for needle: {0}".format(Query.needle)) log("") Query.reset() return completions
def on_project_focus(window): """ window has gained focus, rebuild file cache (in case files were removed/added) """ verbose(ID, "refocus project") for folder in window.folders(): state.rebuild_filecache(folder)
def finish_tracking(self, view, command_name=None): self.track_insert["active"] = False verbose(ID, "finish tracking")
def on_file_created(): """ a new file has been created """ verbose(ID, "file created -- rebuild cache") state.update() state.rebuild_filecache()
def on_query_completions(view, project_folder, current_folder): global Context, Selection current_scope = Selection.get_scope(view) if not Query.by_command(): triggers = get_matching_autotriggers(current_scope, config["TRIGGER"]) else: triggers = config["TRIGGER"] if not bool(triggers): verbose(ID, "abort query, no valid scope-regex for current context") return False # parse current context, may contain 'is_valid: False' expression = Context.get_context(view) if expression["error"] and not Query.by_command(): verbose(ID, "abort not a valid context") return False # check if there is a trigger for the current expression trigger = Context.find_trigger(expression, current_scope, triggers) # verbose("trigger", trigger) # expression | trigger | force | ACTION | CURRENT # -----------|----------|-------|-------------------|-------- # invalid | False | False | abort | abort # invalid | False | True | query needle | abort # invalid | True | False | query | # invalid | True | True | query +override | # valid | False | False | abort | abort # valid | False | True | query needle | abort # valid | True | False | query | # valid | True | True | query +override | # currently trigger is required in Query.build if trigger is False: verbose(ID, "abort completion, no trigger found") return False if not expression["valid_needle"]: word = Selection.get_word(view) expression["needle"] = re.sub("[^\.A-Za-z0-9\-\_$]", "", word) verbose( ID, "changed invalid needle to {0}".format(expression["needle"])) else: verbose(ID, "context evaluation {0}".format(expression)) if Query.build(expression.get("needle"), trigger, current_folder, project_folder) is False: # query is valid, but may not be triggered: not forced, no auto-options verbose(ID, "abort valid query: auto trigger disabled") return False verbose( ID, ".───────────────────────────────────────────────────────────────") verbose(ID, "| scope settings: {0}".format(trigger)) verbose(ID, "| search needle: '{0}'".format(Query.needle)) verbose(ID, "| in base path: '{0}'".format(Query.base_path)) FuzzyFilePath.start_expression = expression completions = ProjectManager.search_completions( Query.needle, project_folder, Query.extensions, Query.base_path) if completions and len(completions[0]) > 0: Completion.start(Query.replace_on_insert) view.run_command('_enter_insert_mode') # vintageous log("| => {0} completions found".format(len(completions))) else: sublime.status_message("FFP no filepaths found for '" + Query.needle + "'") Completion.stop() log("| => no valid files found for needle: {0}".format( Query.needle)) log("") Query.reset() return completions
def rebuild_filecache(self): verbose(ID, "rebuild filecache of ", self.project_directory) return self.filecache.rebuild()