From c13792291818dd12ca285f7c26abfc2eed8c202b Mon Sep 17 00:00:00 2001 From: Holger Rapp Date: Thu, 13 Feb 2014 21:20:07 +0100 Subject: [PATCH] Create UltiSnips.UltiSnips_Manager in all cases. If a python module tried to load UltiSnips without the autoload file being run first, UltiSnips_Manager was not defined. This broke YCM and maybe others too. This fixes https://github.com/Valloric/YouCompleteMe/issues/820. --- autoload/UltiSnips.vim | 11 +- pythonx/UltiSnips/__init__.py | 739 +-------------------------- pythonx/UltiSnips/snippet_manager.py | 738 ++++++++++++++++++++++++++ 3 files changed, 752 insertions(+), 736 deletions(-) create mode 100755 pythonx/UltiSnips/snippet_manager.py diff --git a/autoload/UltiSnips.vim b/autoload/UltiSnips.vim index 20de07a..18d80de 100644 --- a/autoload/UltiSnips.vim +++ b/autoload/UltiSnips.vim @@ -13,6 +13,11 @@ endif function! UltiSnips_FileTypeChanged() endfunction +" Kludge to make sure that if the python module is loaded first, all of this +" initialization in this file is indeed done. +function! UltiSnips#EnsureAutoloadScriptWasRun() +endfunction + if !exists("g:UltiSnipsUsePythonVersion") let g:_uspy=":py3 " if !has("python3") @@ -274,10 +279,6 @@ exec g:_uspy "new_path = os.path.abspath(os.path.join( \ vim.eval('expand(\":h\")'), '..', 'pythonx'))" exec g:_uspy "vim.command(\"let g:UltiSnipsPythonPath = '%s'\" % new_path)" exec g:_uspy "if not hasattr(vim, 'VIM_SPECIAL_PATH'): sys.path.append(new_path)" -exec g:_uspy "from UltiSnips import SnippetManager" -exec g:_uspy "UltiSnips_Manager = SnippetManager( - \ vim.eval('g:UltiSnipsExpandTrigger'), - \ vim.eval('g:UltiSnipsJumpForwardTrigger'), - \ vim.eval('g:UltiSnipsJumpBackwardTrigger'))" +exec g:_uspy "from UltiSnips import UltiSnips_Manager" let did_UltiSnips_autoload=1 diff --git a/pythonx/UltiSnips/__init__.py b/pythonx/UltiSnips/__init__.py index 5733611..69aa204 100755 --- a/pythonx/UltiSnips/__init__.py +++ b/pythonx/UltiSnips/__init__.py @@ -1,738 +1,15 @@ #!/usr/bin/env python # encoding: utf-8 -"""Contains the SnippetManager facade used by all Vim Functions.""" +"""Entry point for all thinks UltiSnips.""" -from collections import defaultdict -from functools import wraps -import glob -import os -import re -import traceback +import vim # pylint:disable=import-error -from UltiSnips._diff import diff, guess_edit -from UltiSnips.compatibility import as_unicode -from UltiSnips.position import Position -from UltiSnips.snippet import Snippet -from UltiSnips.snippet_definitions import parse_snippets_file -from UltiSnips.snippet_dictionary import SnippetDictionary -from UltiSnips.vim_state import VimState, VisualContentPreserver -import UltiSnips._vim as _vim +from UltiSnips.snippet_manager import SnippetManager -def _ask_snippets(snippets): - """ Given a list of snippets, ask the user which one they - want to use, and return it. - """ - display = [as_unicode("%i: %s") % (i+1, s.description) for - i, s in enumerate(snippets)] - try: - rv = _vim.eval("inputlist(%s)" % _vim.escape(display)) - if rv is None or rv == '0': - return None - rv = int(rv) - if rv > len(snippets): - rv = len(snippets) - return snippets[rv-1] - except _vim.error: - # Likely "invalid expression", but might be translated. We have no way - # of knowing the exact error, therefore, we ignore all errors silently. - return None - except KeyboardInterrupt: - return None +vim.command("call UltiSnips#EnsureAutoloadScriptWasRun()") -def _base_snippet_files_for(ft, default=True): - """Returns a list of snippet files matching the given filetype (ft). - If default is set to false, it doesn't include shipped files. - - Searches through each path in 'runtimepath' in reverse order, - in each of these, it searches each directory name listed in - 'g:UltiSnipsSnippetDirectories' in order, then looks for files in these - directories called 'ft.snippets' or '*_ft.snippets' replacing ft with - the filetype. - """ - - if _vim.eval("exists('b:UltiSnipsSnippetDirectories')") == "1": - snippet_dirs = _vim.eval("b:UltiSnipsSnippetDirectories") - else: - snippet_dirs = _vim.eval("g:UltiSnipsSnippetDirectories") - base_snippets = os.path.realpath(os.path.join( - __file__, "../../../UltiSnips")) - ret = [] - - paths = _vim.eval("&runtimepath").split(',') - - if _should_reverse_search_path(): - paths = paths[::-1] - - for rtp in paths: - for snippet_dir in snippet_dirs: - pth = os.path.realpath(os.path.expanduser( - os.path.join(rtp, snippet_dir))) - patterns = ["%s.snippets", "%s_*.snippets", os.path.join("%s", "*")] - if not default and pth == base_snippets: - patterns.remove("%s.snippets") - - for pattern in patterns: - for fn in glob.glob(os.path.join(pth, pattern % ft)): - if fn not in ret: - ret.append(fn) - return ret - - -def _plugin_dir(): - """Calculates the plugin directory for UltiSnips.""" - directory = __file__ - for _ in range(10): - directory = os.path.dirname(directory) - if (os.path.isdir(os.path.join(directory, "plugin")) and - os.path.isdir(os.path.join(directory, "doc"))): - return directory - raise Exception("Unable to find the plugin directory.") - -def _snippets_dir_is_before_plugin_dir(): - """ Returns True if the snippets directory comes before the plugin - directory in Vim's runtime path. False otherwise. - """ - paths = [os.path.realpath(os.path.expanduser(p)).rstrip(os.path.sep) - for p in _vim.eval("&runtimepath").split(',')] - home = _vim.eval("$HOME") - def vim_path_index(suffix): - """Returns index of 'suffix' in 'paths' or -1 if it is not found.""" - path = os.path.realpath(os.path.join(home, suffix)).rstrip(os.path.sep) - try: - return paths.index(path) - except ValueError: - return -1 - try: - real_vim_path_index = max( - vim_path_index(".vim"), vim_path_index("vimfiles")) - plugin_path_index = paths.index(_plugin_dir()) - return plugin_path_index < real_vim_path_index - except ValueError: - return False - -def _should_reverse_search_path(): - """ If the user defined g:UltiSnipsDontReverseSearchPath then return True - or False based on the value of that variable, else defer to - _snippets_dir_is_before_plugin_dir to determine whether this is True or - False. - """ - if _vim.eval("exists('g:UltiSnipsDontReverseSearchPath')") != "0": - return _vim.eval("g:UltiSnipsDontReverseSearchPath") != "0" - return not _snippets_dir_is_before_plugin_dir() - -def err_to_scratch_buffer(func): - """Decorator that will catch any Exception that 'func' throws and displays - it in a new Vim scratch buffer.""" - @wraps(func) - def wrapper(self, *args, **kwds): - try: - return func(self, *args, **kwds) - except: # pylint: disable=bare-except - msg = \ -"""An error occured. This is either a bug in UltiSnips or a bug in a -snippet definition. If you think this is a bug, please report it to -https://bugs.launchpad.net/ultisnips/+filebug. - -Following is the full stack trace: -""" - msg += traceback.format_exc() - self.leaving_buffer() # Vim sends no WinLeave msg here. - _vim.new_scratch_buffer(msg) - return wrapper - - -# TODO(sirver): This class has too many responsibilities - it should not also -# care for the parsing and managing of parsed snippets. -class SnippetManager(object): - """The main entry point for all UltiSnips functionality. All Vim functions - call methods in this class.""" - - def __init__(self, expand_trigger, forward_trigger, backward_trigger): - self.expand_trigger = expand_trigger - self.forward_trigger = forward_trigger - self.backward_trigger = backward_trigger - self._supertab_keys = None - self._csnippets = [] - self.reset() - - @err_to_scratch_buffer - def reset(self, test_error=False): - """Reset the class to the state it had directly after creation.""" - self._vstate = VimState() - self._test_error = test_error - self._snippets = defaultdict(SnippetDictionary) - self._filetypes = defaultdict(lambda: ['all']) - self._visual_content = VisualContentPreserver() - - while len(self._csnippets): - self._current_snippet_is_done() - - self._reinit() - - @err_to_scratch_buffer - def jump_forwards(self): - """Jumps to the next tabstop.""" - _vim.command("let g:ulti_jump_forwards_res = 1") - if not self._jump(): - _vim.command("let g:ulti_jump_forwards_res = 0") - return self._handle_failure(self.forward_trigger) - - @err_to_scratch_buffer - def jump_backwards(self): - """Jumps to the previous tabstop.""" - _vim.command("let g:ulti_jump_backwards_res = 1") - if not self._jump(True): - _vim.command("let g:ulti_jump_backwards_res = 0") - return self._handle_failure(self.backward_trigger) - - @err_to_scratch_buffer - def expand(self): - """Trie to expand a snippet at the current position.""" - _vim.command("let g:ulti_expand_res = 1") - if not self._try_expand(): - _vim.command("let g:ulti_expand_res = 0") - self._handle_failure(self.expand_trigger) - - @err_to_scratch_buffer - def snippets_in_current_scope(self): - """Returns the snippets that could be expanded to Vim as a global - variable.""" - before = _vim.buf.line_till_cursor - snippets = self._snips(before, True) - - # Sort snippets alphabetically - snippets.sort(key=lambda x: x.trigger) - for snip in snippets: - description = snip.description[snip.description.find(snip.trigger) + - len(snip.trigger) + 2:] - - key = as_unicode(snip.trigger) - description = as_unicode(description) - - #remove surrounding "" or '' in snippet description if it exists - if len(description) > 2: - if (description[0] == description[-1] and - description[0] in "'\""): - description = description[1:-1] - - _vim.command(as_unicode( - "let g:current_ulti_dict['{key}'] = '{val}'").format( - key=key.replace("'", "''"), - val=description.replace("'", "''"))) - - @err_to_scratch_buffer - def list_snippets(self): - """Shows the snippets that could be expanded to the User and let her - select one.""" - before = _vim.buf.line_till_cursor - snippets = self._snips(before, True) - - if len(snippets) == 0: - self._handle_failure(self.backward_trigger) - return True - - # Sort snippets alphabetically - snippets.sort(key=lambda x: x.trigger) - - if not snippets: - return True - - snippet = _ask_snippets(snippets) - if not snippet: - return True - - self._do_snippet(snippet, before) - - return True - - @err_to_scratch_buffer - def expand_or_jump(self): - """ - This function is used for people who wants to have the same trigger for - expansion and forward jumping. It first tries to expand a snippet, if - this fails, it tries to jump forward. - """ - _vim.command('let g:ulti_expand_or_jump_res = 1') - rv = self._try_expand() - if not rv: - _vim.command('let g:ulti_expand_or_jump_res = 2') - rv = self._jump() - if not rv: - _vim.command('let g:ulti_expand_or_jump_res = 0') - self._handle_failure(self.expand_trigger) - - @err_to_scratch_buffer - def save_last_visual_selection(self): - """ - This is called when the expand trigger is pressed in visual mode. - Our job is to remember everything between '< and '> and pass it on to - ${VISUAL} in case it will be needed. - """ - self._visual_content.conserve() - - @err_to_scratch_buffer - def add_snippet(self, trigger, value, description, - options, ft="all", globals=None, fn=None): - """Add a snippet to the list of known snippets of the given 'ft'.""" - self._snippets[ft].add_snippet( - Snippet(trigger, value, description, options, globals or {}), fn - ) - - @err_to_scratch_buffer - def expand_anon(self, value, trigger="", description="", - options="", globals=None): - """Expand an anonymous snippet right here.""" - if globals is None: - globals = {} - - before = _vim.buf.line_till_cursor - snip = Snippet(trigger, value, description, options, globals) - - if not trigger or snip.matches(before): - self._do_snippet(snip, before) - return True - else: - return False - - @err_to_scratch_buffer - def cursor_moved(self): - """Called whenever the cursor moved.""" - self._vstate.remember_position() - if _vim.eval("mode()") not in 'in': - return - - if self._ignore_movements: - self._ignore_movements = False - return - - if self._csnippets: - cstart = self._csnippets[0].start.line - cend = self._csnippets[0].end.line + \ - self._vstate.diff_in_buffer_length - ct = _vim.buf[cstart:cend + 1] - lt = self._vstate.remembered_buffer - pos = _vim.buf.cursor - - lt_span = [0, len(lt)] - ct_span = [0, len(ct)] - initial_line = cstart - - # Cut down on lines searched for changes. Start from behind and - # remove all equal lines. Then do the same from the front. - if lt and ct: - while (lt[lt_span[1]-1] == ct[ct_span[1]-1] and - self._vstate.ppos.line < initial_line + lt_span[1]-1 and - pos.line < initial_line + ct_span[1]-1 and - (lt_span[0] < lt_span[1]) and - (ct_span[0] < ct_span[1])): - ct_span[1] -= 1 - lt_span[1] -= 1 - while (lt_span[0] < lt_span[1] and - ct_span[0] < ct_span[1] and - lt[lt_span[0]] == ct[ct_span[0]] and - self._vstate.ppos.line >= initial_line and - pos.line >= initial_line): - ct_span[0] += 1 - lt_span[0] += 1 - initial_line += 1 - ct_span[0] = max(0, ct_span[0] - 1) - lt_span[0] = max(0, lt_span[0] - 1) - initial_line = max(cstart, initial_line - 1) - - lt = lt[lt_span[0]:lt_span[1]] - ct = ct[ct_span[0]:ct_span[1]] - - try: - rv, es = guess_edit(initial_line, lt, ct, self._vstate) - if not rv: - lt = '\n'.join(lt) - ct = '\n'.join(ct) - es = diff(lt, ct, initial_line) - self._csnippets[0].replay_user_edits(es) - except IndexError: - # Rather do nothing than throwing an error. It will be correct - # most of the time - pass - - self._check_if_still_inside_snippet() - if self._csnippets: - self._csnippets[0].update_textobjects() - self._vstate.remember_buffer(self._csnippets[0]) - - def leaving_buffer(self): - """Called when the user switches tabs/windows/buffers. It basically - means that all snippets must be properly terminated.""" - while len(self._csnippets): - self._current_snippet_is_done() - self._reinit() - - ################################### - # Private/Protect Functions Below # - ################################### - def _report_error(self, msg): - """Shows 'msg' as error to the user.""" - msg = _vim.escape("UltiSnips: " + msg) - if self._test_error: - msg = msg.replace('"', r'\"') - msg = msg.replace('|', r'\|') - _vim.command("let saved_pos=getpos('.')") - _vim.command("$:put =%s" % msg) - _vim.command("call setpos('.', saved_pos)") - elif False: - _vim.command("echohl WarningMsg") - _vim.command("echomsg %s" % msg) - _vim.command("echohl None") - else: - _vim.command("echoerr %s" % msg) - - def _add_extending_info(self, ft, parents): - """Add the list of 'parents' as being extended by the 'ft'.""" - sd = self._snippets[ft] - for parent in parents: - if parent in sd.extends: - continue - sd.extends.append(parent) - - def _reinit(self): - """Resets transient state.""" - self._ctab = None - self._ignore_movements = False - - def _check_if_still_inside_snippet(self): - """Checks if the cursor is outside of the current snippet.""" - if self._cs and ( - not self._cs.start <= _vim.buf.cursor <= self._cs.end - ): - self._current_snippet_is_done() - self._reinit() - self._check_if_still_inside_snippet() - - def _current_snippet_is_done(self): - """The current snippet should be terminated.""" - self._csnippets.pop() - if (not self._csnippets and - _vim.eval("g:UltiSnipsClearJumpTrigger") != "0"): - _vim.command("call UltiSnips#RestoreInnerKeys()") - - def _jump(self, backwards=False): - """Helper method that does the actual jump.""" - jumped = False - if self._cs: - self._ctab = self._cs.select_next_tab(backwards) - if self._ctab: - if self._cs.snippet.has_option("s"): - lineno = _vim.buf.cursor.line - _vim.buf[lineno] = _vim.buf[lineno].rstrip() - _vim.select(self._ctab.start, self._ctab.end) - jumped = True - if self._ctab.number == 0: - self._current_snippet_is_done() - else: - # This really shouldn't happen, because a snippet should - # have been popped when its final tabstop was used. - # Cleanup by removing current snippet and recursing. - self._current_snippet_is_done() - jumped = self._jump(backwards) - if jumped: - self._vstate.remember_position() - self._vstate.remember_unnamed_register(self._ctab.current_text) - self._ignore_movements = True - return jumped - - def leaving_insert_mode(self): - """Called whenever we leave the insert mode.""" - self._vstate.restore_unnamed_register() - - def _handle_failure(self, trigger): - """Mainly make sure that we play well with SuperTab.""" - if trigger.lower() == "": - feedkey = "\\" + trigger - elif trigger.lower() == "": - feedkey = "\\" + trigger - else: - feedkey = None - mode = "n" - if not self._supertab_keys: - if _vim.eval("exists('g:SuperTabMappingForward')") != "0": - self._supertab_keys = ( - _vim.eval("g:SuperTabMappingForward"), - _vim.eval("g:SuperTabMappingBackward"), - ) - else: - self._supertab_keys = ['', ''] - - for idx, sttrig in enumerate(self._supertab_keys): - if trigger.lower() == sttrig.lower(): - if idx == 0: - feedkey = r"\SuperTabForward" - mode = "n" - elif idx == 1: - feedkey = r"\SuperTabBackward" - mode = "p" - # Use remap mode so SuperTab mappings will be invoked. - break - - if (feedkey == r"\SuperTabForward" or - feedkey == r"\SuperTabBackward"): - _vim.command("return SuperTab(%s)" % _vim.escape(mode)) - elif feedkey: - _vim.command("return %s" % _vim.escape(feedkey)) - - def _snips(self, before, possible): - """ Returns all the snippets for the given text - before the cursor. If possible is True, then get all - possible matches. - """ - self._ensure_all_loaded() - filetypes = self._filetypes[_vim.buf.number][::-1] - - found_snippets = [] - for ft in filetypes: - found_snippets += self._find_snippets(ft, before, possible) - - # Search if any of the snippets overwrites the previous - # Dictionary allows O(1) access for easy overwrites - snippets = {} - for snip in found_snippets: - if (snip.trigger not in snippets) or snip.overwrites_previous: - snippets[snip.trigger] = [] - snippets[snip.trigger].append(snip) - - # Transform dictionary into flat list of snippets - selected_snippets = set( - [item for sublist in snippets.values() for item in sublist]) - # Return snippets to their original order - snippets = [snip for snip in found_snippets if - snip in selected_snippets] - - return snippets - - def _do_snippet(self, snippet, before): - """Expands the given snippet, and handles everything - that needs to be done with it.""" - if _vim.eval("g:UltiSnipsClearJumpTrigger") == "1": - _vim.command("call UltiSnips#MapInnerKeys()") - # Adjust before, maybe the trigger is not the complete word - text_before = before - if snippet.matched: - text_before = before[:-len(snippet.matched)] - - if self._cs: - start = Position(_vim.buf.cursor.line, len(text_before)) - end = Position(_vim.buf.cursor.line, len(before)) - - # It could be that our trigger contains the content of TextObjects - # in our containing snippet. If this is indeed the case, we have to - # make sure that those are properly killed. We do this by - # pretending that the user deleted and retyped the text that our - # trigger matched. - edit_actions = [ - ("D", start.line, start.col, snippet.matched), - ("I", start.line, start.col, snippet.matched), - ] - self._csnippets[0].replay_user_edits(edit_actions) - - si = snippet.launch(text_before, self._visual_content, - self._cs.find_parent_for_new_to(start), start, end) - else: - start = Position(_vim.buf.cursor.line, len(text_before)) - end = Position(_vim.buf.cursor.line, len(before)) - si = snippet.launch(text_before, self._visual_content, - None, start, end) - - self._visual_content.reset() - self._csnippets.append(si) - - self._ignore_movements = True - self._vstate.remember_buffer(self._csnippets[0]) - - self._jump() - - def _try_expand(self): - """Try to expand a snippet in the current place.""" - before = _vim.buf.line_till_cursor - if not before: - return False - snippets = self._snips(before, False) - if not snippets: - # No snippet found - return False - elif len(snippets) == 1: - snippet = snippets[0] - else: - snippet = _ask_snippets(snippets) - if not snippet: - return True - self._do_snippet(snippet, before) - return True - - @property - def _cs(self): - """The current snippet or None.""" - if not len(self._csnippets): - return None - return self._csnippets[-1] - - def _parse_snippets(self, ft, filename, file_data=None): - """Parse the file 'filename' for the given 'ft' and watch it for - changes in the future. 'file_data' can be injected in tests.""" - self._snippets[ft].addfile(filename) - if file_data is None: - file_data = open(filename, "r").read() - for event, data in parse_snippets_file(file_data): - if event == "error": - msg, line_index = data - filename = _vim.eval("""fnamemodify(%s, ":~:.")""" % - _vim.escape(filename)) - self._report_error("%s in %s(%d)" % (msg, filename, line_index)) - break - elif event == "clearsnippets": - triggers, = data - self._snippets[ft].clear_snippets(triggers) - elif event == "extends": - filetypes, = data - self._add_extending_info(ft, filetypes) - elif event == "snippet": - trigger, value, descr, opts, globals = data - self.add_snippet(trigger, value, descr, - opts, ft, globals, filename) - else: - assert False, "Unhandled %s: %r" % (event, data) - - @property - def primary_filetype(self): - """This filetype will be edited when UltiSnipsEdit is called without - any arguments.""" - return self._filetypes[_vim.buf.number][0] - - def file_to_edit(self, ft): # pylint: disable=no-self-use - """ Gets a file to edit based on the given filetype. - If no filetype is given, uses the current filetype from Vim. - - Checks 'g:UltiSnipsSnippetsDir' and uses it if it exists - If a non-shipped file already exists, it uses it. - Otherwise uses a file in ~/.vim/ or ~/vimfiles - """ - # This method is not using self, but is called by UltiSnips.vim and is - # therefore in this class because it is the facade to Vim. - edit = None - existing = _base_snippet_files_for(ft, False) - filename = ft + ".snippets" - - if _vim.eval("exists('g:UltiSnipsSnippetsDir')") == "1": - snipdir = _vim.eval("g:UltiSnipsSnippetsDir") - edit = os.path.join(snipdir, filename) - elif existing: - edit = existing[-1] # last sourced/highest priority - else: - home = _vim.eval("$HOME") - rtp = [os.path.realpath(os.path.expanduser(p)) - for p in _vim.eval("&rtp").split(",")] - snippet_dirs = ["UltiSnips"] + \ - _vim.eval("g:UltiSnipsSnippetDirectories") - us = snippet_dirs[-1] - - path = os.path.join(home, ".vim", us) - for dirname in [".vim", "vimfiles"]: - pth = os.path.join(home, dirname) - if pth in rtp: - path = os.path.join(pth, us) - - if not os.path.isdir(path): - os.mkdir(path) - - edit = os.path.join(path, filename) - return edit - - def _load_snippets_for(self, ft): - """Load all snippets for the given 'ft'.""" - if ft in self._snippets: - del self._snippets[ft] - for fn in _base_snippet_files_for(ft): - self._parse_snippets(ft, fn) - # Now load for the parents - for parent_ft in self._snippets[ft].extends: - if parent_ft not in self._snippets: - self._load_snippets_for(parent_ft) - - def _needs_update(self, ft): - """Returns true if any files for 'ft' have changed and must be - reloaded.""" - do_hash = _vim.eval('exists("g:UltiSnipsDoHash")') == "0" \ - or _vim.eval("g:UltiSnipsDoHash") != "0" - - if ft not in self._snippets: - return True - elif do_hash and self._snippets[ft].has_any_file_changed(): - return True - elif do_hash: - cur_snips = set(_base_snippet_files_for(ft)) - old_snips = set(self._snippets[ft].files) - if cur_snips - old_snips: - return True - return False - - def _ensure_loaded(self, ft, checked=None): - """Make sure that the snippets for 'ft' and everything it extends are - loaded.""" - if not checked: - checked = set([ft]) - elif ft in checked: - return - else: - checked.add(ft) - - if self._needs_update(ft): - self._load_snippets_for(ft) - - for parent in self._snippets[ft].extends: - self._ensure_loaded(parent, checked) - - def _ensure_all_loaded(self): - """Make sure that all filetypes fur the current buffer are loaded.""" - for ft in self._filetypes[_vim.buf.number]: - self._ensure_loaded(ft) - - def reset_buffer_filetypes(self): - """Reset the filetypes for the current buffer.""" - if _vim.buf.number in self._filetypes: - del self._filetypes[_vim.buf.number] - - def add_buffer_filetypes(self, ft): - """Checks for changes in the list of snippet files or the contents of - the snippet files and reloads them if necessary. """ - buf_fts = self._filetypes[_vim.buf.number] - idx = -1 - for ft in ft.split("."): - ft = ft.strip() - if not ft: - continue - try: - idx = buf_fts.index(ft) - except ValueError: - self._filetypes[_vim.buf.number].insert(idx + 1, ft) - idx += 1 - - def _find_snippets(self, ft, trigger, potentially=False, seen=None): - """Find snippets matching trigger - - ft - file type to search - trigger - trigger to match against - potentially - also returns snippets that could potentially match; that - is which triggers start with the current trigger - """ - snips = self._snippets.get(ft, None) - if not snips: - return [] - if not seen: - seen = set() - seen.add(ft) - parent_results = [] - for parent_ft in snips.extends: - if parent_ft not in seen: - seen.add(parent_ft) - parent_results += self._find_snippets(parent_ft, trigger, - potentially, seen) - return parent_results + snips.get_matching_snippets( - trigger, potentially) +UltiSnips_Manager = SnippetManager( # pylint:disable=invalid-name + vim.eval('g:UltiSnipsExpandTrigger'), + vim.eval('g:UltiSnipsJumpForwardTrigger'), + vim.eval('g:UltiSnipsJumpBackwardTrigger')) diff --git a/pythonx/UltiSnips/snippet_manager.py b/pythonx/UltiSnips/snippet_manager.py new file mode 100755 index 0000000..5733611 --- /dev/null +++ b/pythonx/UltiSnips/snippet_manager.py @@ -0,0 +1,738 @@ +#!/usr/bin/env python +# encoding: utf-8 + +"""Contains the SnippetManager facade used by all Vim Functions.""" + +from collections import defaultdict +from functools import wraps +import glob +import os +import re +import traceback + +from UltiSnips._diff import diff, guess_edit +from UltiSnips.compatibility import as_unicode +from UltiSnips.position import Position +from UltiSnips.snippet import Snippet +from UltiSnips.snippet_definitions import parse_snippets_file +from UltiSnips.snippet_dictionary import SnippetDictionary +from UltiSnips.vim_state import VimState, VisualContentPreserver +import UltiSnips._vim as _vim + +def _ask_snippets(snippets): + """ Given a list of snippets, ask the user which one they + want to use, and return it. + """ + display = [as_unicode("%i: %s") % (i+1, s.description) for + i, s in enumerate(snippets)] + try: + rv = _vim.eval("inputlist(%s)" % _vim.escape(display)) + if rv is None or rv == '0': + return None + rv = int(rv) + if rv > len(snippets): + rv = len(snippets) + return snippets[rv-1] + except _vim.error: + # Likely "invalid expression", but might be translated. We have no way + # of knowing the exact error, therefore, we ignore all errors silently. + return None + except KeyboardInterrupt: + return None + +def _base_snippet_files_for(ft, default=True): + """Returns a list of snippet files matching the given filetype (ft). + If default is set to false, it doesn't include shipped files. + + Searches through each path in 'runtimepath' in reverse order, + in each of these, it searches each directory name listed in + 'g:UltiSnipsSnippetDirectories' in order, then looks for files in these + directories called 'ft.snippets' or '*_ft.snippets' replacing ft with + the filetype. + """ + + if _vim.eval("exists('b:UltiSnipsSnippetDirectories')") == "1": + snippet_dirs = _vim.eval("b:UltiSnipsSnippetDirectories") + else: + snippet_dirs = _vim.eval("g:UltiSnipsSnippetDirectories") + base_snippets = os.path.realpath(os.path.join( + __file__, "../../../UltiSnips")) + ret = [] + + paths = _vim.eval("&runtimepath").split(',') + + if _should_reverse_search_path(): + paths = paths[::-1] + + for rtp in paths: + for snippet_dir in snippet_dirs: + pth = os.path.realpath(os.path.expanduser( + os.path.join(rtp, snippet_dir))) + patterns = ["%s.snippets", "%s_*.snippets", os.path.join("%s", "*")] + if not default and pth == base_snippets: + patterns.remove("%s.snippets") + + for pattern in patterns: + for fn in glob.glob(os.path.join(pth, pattern % ft)): + if fn not in ret: + ret.append(fn) + return ret + + +def _plugin_dir(): + """Calculates the plugin directory for UltiSnips.""" + directory = __file__ + for _ in range(10): + directory = os.path.dirname(directory) + if (os.path.isdir(os.path.join(directory, "plugin")) and + os.path.isdir(os.path.join(directory, "doc"))): + return directory + raise Exception("Unable to find the plugin directory.") + +def _snippets_dir_is_before_plugin_dir(): + """ Returns True if the snippets directory comes before the plugin + directory in Vim's runtime path. False otherwise. + """ + paths = [os.path.realpath(os.path.expanduser(p)).rstrip(os.path.sep) + for p in _vim.eval("&runtimepath").split(',')] + home = _vim.eval("$HOME") + def vim_path_index(suffix): + """Returns index of 'suffix' in 'paths' or -1 if it is not found.""" + path = os.path.realpath(os.path.join(home, suffix)).rstrip(os.path.sep) + try: + return paths.index(path) + except ValueError: + return -1 + try: + real_vim_path_index = max( + vim_path_index(".vim"), vim_path_index("vimfiles")) + plugin_path_index = paths.index(_plugin_dir()) + return plugin_path_index < real_vim_path_index + except ValueError: + return False + +def _should_reverse_search_path(): + """ If the user defined g:UltiSnipsDontReverseSearchPath then return True + or False based on the value of that variable, else defer to + _snippets_dir_is_before_plugin_dir to determine whether this is True or + False. + """ + if _vim.eval("exists('g:UltiSnipsDontReverseSearchPath')") != "0": + return _vim.eval("g:UltiSnipsDontReverseSearchPath") != "0" + return not _snippets_dir_is_before_plugin_dir() + +def err_to_scratch_buffer(func): + """Decorator that will catch any Exception that 'func' throws and displays + it in a new Vim scratch buffer.""" + @wraps(func) + def wrapper(self, *args, **kwds): + try: + return func(self, *args, **kwds) + except: # pylint: disable=bare-except + msg = \ +"""An error occured. This is either a bug in UltiSnips or a bug in a +snippet definition. If you think this is a bug, please report it to +https://bugs.launchpad.net/ultisnips/+filebug. + +Following is the full stack trace: +""" + msg += traceback.format_exc() + self.leaving_buffer() # Vim sends no WinLeave msg here. + _vim.new_scratch_buffer(msg) + return wrapper + + +# TODO(sirver): This class has too many responsibilities - it should not also +# care for the parsing and managing of parsed snippets. +class SnippetManager(object): + """The main entry point for all UltiSnips functionality. All Vim functions + call methods in this class.""" + + def __init__(self, expand_trigger, forward_trigger, backward_trigger): + self.expand_trigger = expand_trigger + self.forward_trigger = forward_trigger + self.backward_trigger = backward_trigger + self._supertab_keys = None + self._csnippets = [] + self.reset() + + @err_to_scratch_buffer + def reset(self, test_error=False): + """Reset the class to the state it had directly after creation.""" + self._vstate = VimState() + self._test_error = test_error + self._snippets = defaultdict(SnippetDictionary) + self._filetypes = defaultdict(lambda: ['all']) + self._visual_content = VisualContentPreserver() + + while len(self._csnippets): + self._current_snippet_is_done() + + self._reinit() + + @err_to_scratch_buffer + def jump_forwards(self): + """Jumps to the next tabstop.""" + _vim.command("let g:ulti_jump_forwards_res = 1") + if not self._jump(): + _vim.command("let g:ulti_jump_forwards_res = 0") + return self._handle_failure(self.forward_trigger) + + @err_to_scratch_buffer + def jump_backwards(self): + """Jumps to the previous tabstop.""" + _vim.command("let g:ulti_jump_backwards_res = 1") + if not self._jump(True): + _vim.command("let g:ulti_jump_backwards_res = 0") + return self._handle_failure(self.backward_trigger) + + @err_to_scratch_buffer + def expand(self): + """Trie to expand a snippet at the current position.""" + _vim.command("let g:ulti_expand_res = 1") + if not self._try_expand(): + _vim.command("let g:ulti_expand_res = 0") + self._handle_failure(self.expand_trigger) + + @err_to_scratch_buffer + def snippets_in_current_scope(self): + """Returns the snippets that could be expanded to Vim as a global + variable.""" + before = _vim.buf.line_till_cursor + snippets = self._snips(before, True) + + # Sort snippets alphabetically + snippets.sort(key=lambda x: x.trigger) + for snip in snippets: + description = snip.description[snip.description.find(snip.trigger) + + len(snip.trigger) + 2:] + + key = as_unicode(snip.trigger) + description = as_unicode(description) + + #remove surrounding "" or '' in snippet description if it exists + if len(description) > 2: + if (description[0] == description[-1] and + description[0] in "'\""): + description = description[1:-1] + + _vim.command(as_unicode( + "let g:current_ulti_dict['{key}'] = '{val}'").format( + key=key.replace("'", "''"), + val=description.replace("'", "''"))) + + @err_to_scratch_buffer + def list_snippets(self): + """Shows the snippets that could be expanded to the User and let her + select one.""" + before = _vim.buf.line_till_cursor + snippets = self._snips(before, True) + + if len(snippets) == 0: + self._handle_failure(self.backward_trigger) + return True + + # Sort snippets alphabetically + snippets.sort(key=lambda x: x.trigger) + + if not snippets: + return True + + snippet = _ask_snippets(snippets) + if not snippet: + return True + + self._do_snippet(snippet, before) + + return True + + @err_to_scratch_buffer + def expand_or_jump(self): + """ + This function is used for people who wants to have the same trigger for + expansion and forward jumping. It first tries to expand a snippet, if + this fails, it tries to jump forward. + """ + _vim.command('let g:ulti_expand_or_jump_res = 1') + rv = self._try_expand() + if not rv: + _vim.command('let g:ulti_expand_or_jump_res = 2') + rv = self._jump() + if not rv: + _vim.command('let g:ulti_expand_or_jump_res = 0') + self._handle_failure(self.expand_trigger) + + @err_to_scratch_buffer + def save_last_visual_selection(self): + """ + This is called when the expand trigger is pressed in visual mode. + Our job is to remember everything between '< and '> and pass it on to + ${VISUAL} in case it will be needed. + """ + self._visual_content.conserve() + + @err_to_scratch_buffer + def add_snippet(self, trigger, value, description, + options, ft="all", globals=None, fn=None): + """Add a snippet to the list of known snippets of the given 'ft'.""" + self._snippets[ft].add_snippet( + Snippet(trigger, value, description, options, globals or {}), fn + ) + + @err_to_scratch_buffer + def expand_anon(self, value, trigger="", description="", + options="", globals=None): + """Expand an anonymous snippet right here.""" + if globals is None: + globals = {} + + before = _vim.buf.line_till_cursor + snip = Snippet(trigger, value, description, options, globals) + + if not trigger or snip.matches(before): + self._do_snippet(snip, before) + return True + else: + return False + + @err_to_scratch_buffer + def cursor_moved(self): + """Called whenever the cursor moved.""" + self._vstate.remember_position() + if _vim.eval("mode()") not in 'in': + return + + if self._ignore_movements: + self._ignore_movements = False + return + + if self._csnippets: + cstart = self._csnippets[0].start.line + cend = self._csnippets[0].end.line + \ + self._vstate.diff_in_buffer_length + ct = _vim.buf[cstart:cend + 1] + lt = self._vstate.remembered_buffer + pos = _vim.buf.cursor + + lt_span = [0, len(lt)] + ct_span = [0, len(ct)] + initial_line = cstart + + # Cut down on lines searched for changes. Start from behind and + # remove all equal lines. Then do the same from the front. + if lt and ct: + while (lt[lt_span[1]-1] == ct[ct_span[1]-1] and + self._vstate.ppos.line < initial_line + lt_span[1]-1 and + pos.line < initial_line + ct_span[1]-1 and + (lt_span[0] < lt_span[1]) and + (ct_span[0] < ct_span[1])): + ct_span[1] -= 1 + lt_span[1] -= 1 + while (lt_span[0] < lt_span[1] and + ct_span[0] < ct_span[1] and + lt[lt_span[0]] == ct[ct_span[0]] and + self._vstate.ppos.line >= initial_line and + pos.line >= initial_line): + ct_span[0] += 1 + lt_span[0] += 1 + initial_line += 1 + ct_span[0] = max(0, ct_span[0] - 1) + lt_span[0] = max(0, lt_span[0] - 1) + initial_line = max(cstart, initial_line - 1) + + lt = lt[lt_span[0]:lt_span[1]] + ct = ct[ct_span[0]:ct_span[1]] + + try: + rv, es = guess_edit(initial_line, lt, ct, self._vstate) + if not rv: + lt = '\n'.join(lt) + ct = '\n'.join(ct) + es = diff(lt, ct, initial_line) + self._csnippets[0].replay_user_edits(es) + except IndexError: + # Rather do nothing than throwing an error. It will be correct + # most of the time + pass + + self._check_if_still_inside_snippet() + if self._csnippets: + self._csnippets[0].update_textobjects() + self._vstate.remember_buffer(self._csnippets[0]) + + def leaving_buffer(self): + """Called when the user switches tabs/windows/buffers. It basically + means that all snippets must be properly terminated.""" + while len(self._csnippets): + self._current_snippet_is_done() + self._reinit() + + ################################### + # Private/Protect Functions Below # + ################################### + def _report_error(self, msg): + """Shows 'msg' as error to the user.""" + msg = _vim.escape("UltiSnips: " + msg) + if self._test_error: + msg = msg.replace('"', r'\"') + msg = msg.replace('|', r'\|') + _vim.command("let saved_pos=getpos('.')") + _vim.command("$:put =%s" % msg) + _vim.command("call setpos('.', saved_pos)") + elif False: + _vim.command("echohl WarningMsg") + _vim.command("echomsg %s" % msg) + _vim.command("echohl None") + else: + _vim.command("echoerr %s" % msg) + + def _add_extending_info(self, ft, parents): + """Add the list of 'parents' as being extended by the 'ft'.""" + sd = self._snippets[ft] + for parent in parents: + if parent in sd.extends: + continue + sd.extends.append(parent) + + def _reinit(self): + """Resets transient state.""" + self._ctab = None + self._ignore_movements = False + + def _check_if_still_inside_snippet(self): + """Checks if the cursor is outside of the current snippet.""" + if self._cs and ( + not self._cs.start <= _vim.buf.cursor <= self._cs.end + ): + self._current_snippet_is_done() + self._reinit() + self._check_if_still_inside_snippet() + + def _current_snippet_is_done(self): + """The current snippet should be terminated.""" + self._csnippets.pop() + if (not self._csnippets and + _vim.eval("g:UltiSnipsClearJumpTrigger") != "0"): + _vim.command("call UltiSnips#RestoreInnerKeys()") + + def _jump(self, backwards=False): + """Helper method that does the actual jump.""" + jumped = False + if self._cs: + self._ctab = self._cs.select_next_tab(backwards) + if self._ctab: + if self._cs.snippet.has_option("s"): + lineno = _vim.buf.cursor.line + _vim.buf[lineno] = _vim.buf[lineno].rstrip() + _vim.select(self._ctab.start, self._ctab.end) + jumped = True + if self._ctab.number == 0: + self._current_snippet_is_done() + else: + # This really shouldn't happen, because a snippet should + # have been popped when its final tabstop was used. + # Cleanup by removing current snippet and recursing. + self._current_snippet_is_done() + jumped = self._jump(backwards) + if jumped: + self._vstate.remember_position() + self._vstate.remember_unnamed_register(self._ctab.current_text) + self._ignore_movements = True + return jumped + + def leaving_insert_mode(self): + """Called whenever we leave the insert mode.""" + self._vstate.restore_unnamed_register() + + def _handle_failure(self, trigger): + """Mainly make sure that we play well with SuperTab.""" + if trigger.lower() == "": + feedkey = "\\" + trigger + elif trigger.lower() == "": + feedkey = "\\" + trigger + else: + feedkey = None + mode = "n" + if not self._supertab_keys: + if _vim.eval("exists('g:SuperTabMappingForward')") != "0": + self._supertab_keys = ( + _vim.eval("g:SuperTabMappingForward"), + _vim.eval("g:SuperTabMappingBackward"), + ) + else: + self._supertab_keys = ['', ''] + + for idx, sttrig in enumerate(self._supertab_keys): + if trigger.lower() == sttrig.lower(): + if idx == 0: + feedkey = r"\SuperTabForward" + mode = "n" + elif idx == 1: + feedkey = r"\SuperTabBackward" + mode = "p" + # Use remap mode so SuperTab mappings will be invoked. + break + + if (feedkey == r"\SuperTabForward" or + feedkey == r"\SuperTabBackward"): + _vim.command("return SuperTab(%s)" % _vim.escape(mode)) + elif feedkey: + _vim.command("return %s" % _vim.escape(feedkey)) + + def _snips(self, before, possible): + """ Returns all the snippets for the given text + before the cursor. If possible is True, then get all + possible matches. + """ + self._ensure_all_loaded() + filetypes = self._filetypes[_vim.buf.number][::-1] + + found_snippets = [] + for ft in filetypes: + found_snippets += self._find_snippets(ft, before, possible) + + # Search if any of the snippets overwrites the previous + # Dictionary allows O(1) access for easy overwrites + snippets = {} + for snip in found_snippets: + if (snip.trigger not in snippets) or snip.overwrites_previous: + snippets[snip.trigger] = [] + snippets[snip.trigger].append(snip) + + # Transform dictionary into flat list of snippets + selected_snippets = set( + [item for sublist in snippets.values() for item in sublist]) + # Return snippets to their original order + snippets = [snip for snip in found_snippets if + snip in selected_snippets] + + return snippets + + def _do_snippet(self, snippet, before): + """Expands the given snippet, and handles everything + that needs to be done with it.""" + if _vim.eval("g:UltiSnipsClearJumpTrigger") == "1": + _vim.command("call UltiSnips#MapInnerKeys()") + # Adjust before, maybe the trigger is not the complete word + text_before = before + if snippet.matched: + text_before = before[:-len(snippet.matched)] + + if self._cs: + start = Position(_vim.buf.cursor.line, len(text_before)) + end = Position(_vim.buf.cursor.line, len(before)) + + # It could be that our trigger contains the content of TextObjects + # in our containing snippet. If this is indeed the case, we have to + # make sure that those are properly killed. We do this by + # pretending that the user deleted and retyped the text that our + # trigger matched. + edit_actions = [ + ("D", start.line, start.col, snippet.matched), + ("I", start.line, start.col, snippet.matched), + ] + self._csnippets[0].replay_user_edits(edit_actions) + + si = snippet.launch(text_before, self._visual_content, + self._cs.find_parent_for_new_to(start), start, end) + else: + start = Position(_vim.buf.cursor.line, len(text_before)) + end = Position(_vim.buf.cursor.line, len(before)) + si = snippet.launch(text_before, self._visual_content, + None, start, end) + + self._visual_content.reset() + self._csnippets.append(si) + + self._ignore_movements = True + self._vstate.remember_buffer(self._csnippets[0]) + + self._jump() + + def _try_expand(self): + """Try to expand a snippet in the current place.""" + before = _vim.buf.line_till_cursor + if not before: + return False + snippets = self._snips(before, False) + if not snippets: + # No snippet found + return False + elif len(snippets) == 1: + snippet = snippets[0] + else: + snippet = _ask_snippets(snippets) + if not snippet: + return True + self._do_snippet(snippet, before) + return True + + @property + def _cs(self): + """The current snippet or None.""" + if not len(self._csnippets): + return None + return self._csnippets[-1] + + def _parse_snippets(self, ft, filename, file_data=None): + """Parse the file 'filename' for the given 'ft' and watch it for + changes in the future. 'file_data' can be injected in tests.""" + self._snippets[ft].addfile(filename) + if file_data is None: + file_data = open(filename, "r").read() + for event, data in parse_snippets_file(file_data): + if event == "error": + msg, line_index = data + filename = _vim.eval("""fnamemodify(%s, ":~:.")""" % + _vim.escape(filename)) + self._report_error("%s in %s(%d)" % (msg, filename, line_index)) + break + elif event == "clearsnippets": + triggers, = data + self._snippets[ft].clear_snippets(triggers) + elif event == "extends": + filetypes, = data + self._add_extending_info(ft, filetypes) + elif event == "snippet": + trigger, value, descr, opts, globals = data + self.add_snippet(trigger, value, descr, + opts, ft, globals, filename) + else: + assert False, "Unhandled %s: %r" % (event, data) + + @property + def primary_filetype(self): + """This filetype will be edited when UltiSnipsEdit is called without + any arguments.""" + return self._filetypes[_vim.buf.number][0] + + def file_to_edit(self, ft): # pylint: disable=no-self-use + """ Gets a file to edit based on the given filetype. + If no filetype is given, uses the current filetype from Vim. + + Checks 'g:UltiSnipsSnippetsDir' and uses it if it exists + If a non-shipped file already exists, it uses it. + Otherwise uses a file in ~/.vim/ or ~/vimfiles + """ + # This method is not using self, but is called by UltiSnips.vim and is + # therefore in this class because it is the facade to Vim. + edit = None + existing = _base_snippet_files_for(ft, False) + filename = ft + ".snippets" + + if _vim.eval("exists('g:UltiSnipsSnippetsDir')") == "1": + snipdir = _vim.eval("g:UltiSnipsSnippetsDir") + edit = os.path.join(snipdir, filename) + elif existing: + edit = existing[-1] # last sourced/highest priority + else: + home = _vim.eval("$HOME") + rtp = [os.path.realpath(os.path.expanduser(p)) + for p in _vim.eval("&rtp").split(",")] + snippet_dirs = ["UltiSnips"] + \ + _vim.eval("g:UltiSnipsSnippetDirectories") + us = snippet_dirs[-1] + + path = os.path.join(home, ".vim", us) + for dirname in [".vim", "vimfiles"]: + pth = os.path.join(home, dirname) + if pth in rtp: + path = os.path.join(pth, us) + + if not os.path.isdir(path): + os.mkdir(path) + + edit = os.path.join(path, filename) + return edit + + def _load_snippets_for(self, ft): + """Load all snippets for the given 'ft'.""" + if ft in self._snippets: + del self._snippets[ft] + for fn in _base_snippet_files_for(ft): + self._parse_snippets(ft, fn) + # Now load for the parents + for parent_ft in self._snippets[ft].extends: + if parent_ft not in self._snippets: + self._load_snippets_for(parent_ft) + + def _needs_update(self, ft): + """Returns true if any files for 'ft' have changed and must be + reloaded.""" + do_hash = _vim.eval('exists("g:UltiSnipsDoHash")') == "0" \ + or _vim.eval("g:UltiSnipsDoHash") != "0" + + if ft not in self._snippets: + return True + elif do_hash and self._snippets[ft].has_any_file_changed(): + return True + elif do_hash: + cur_snips = set(_base_snippet_files_for(ft)) + old_snips = set(self._snippets[ft].files) + if cur_snips - old_snips: + return True + return False + + def _ensure_loaded(self, ft, checked=None): + """Make sure that the snippets for 'ft' and everything it extends are + loaded.""" + if not checked: + checked = set([ft]) + elif ft in checked: + return + else: + checked.add(ft) + + if self._needs_update(ft): + self._load_snippets_for(ft) + + for parent in self._snippets[ft].extends: + self._ensure_loaded(parent, checked) + + def _ensure_all_loaded(self): + """Make sure that all filetypes fur the current buffer are loaded.""" + for ft in self._filetypes[_vim.buf.number]: + self._ensure_loaded(ft) + + def reset_buffer_filetypes(self): + """Reset the filetypes for the current buffer.""" + if _vim.buf.number in self._filetypes: + del self._filetypes[_vim.buf.number] + + def add_buffer_filetypes(self, ft): + """Checks for changes in the list of snippet files or the contents of + the snippet files and reloads them if necessary. """ + buf_fts = self._filetypes[_vim.buf.number] + idx = -1 + for ft in ft.split("."): + ft = ft.strip() + if not ft: + continue + try: + idx = buf_fts.index(ft) + except ValueError: + self._filetypes[_vim.buf.number].insert(idx + 1, ft) + idx += 1 + + def _find_snippets(self, ft, trigger, potentially=False, seen=None): + """Find snippets matching trigger + + ft - file type to search + trigger - trigger to match against + potentially - also returns snippets that could potentially match; that + is which triggers start with the current trigger + """ + snips = self._snippets.get(ft, None) + if not snips: + return [] + if not seen: + seen = set() + seen.add(ft) + parent_results = [] + for parent_ft in snips.extends: + if parent_ft not in seen: + seen.add(parent_ft) + parent_results += self._find_snippets(parent_ft, trigger, + potentially, seen) + return parent_results + snips.get_matching_snippets( + trigger, potentially)