Moving towards Snippet providers.

Removed parsing responsibilities from SnippetManager and instead put
them into the new module providers. Renamed private methods on
SnippetManager that are not meant to be called by external libraries to
start with _. Refactored tests so that expected failures can be tested
and therefore removed the testing flag from SnippetManager.
This commit is contained in:
Holger Rapp 2014-02-14 23:58:00 +01:00
parent a9d946135f
commit 2eb82d127b
11 changed files with 454 additions and 408 deletions

View File

@ -100,7 +100,7 @@ function! s:compensate_for_pum()
""" to explicitly check for the presence of the popup menu, and update
""" the vim-state accordingly.
if pumvisible()
exec g:_uspy "UltiSnips_Manager.cursor_moved()"
exec g:_uspy "UltiSnips_Manager._cursor_moved()"
endif
endfunction
@ -108,9 +108,9 @@ function! UltiSnips#Edit(...)
if a:0 == 1 && a:1 != ''
let type = a:1
else
exec g:_uspy "vim.command(\"let type = '%s'\" % UltiSnips_Manager.primary_filetype)"
exec g:_uspy "vim.command(\"let type = '%s'\" % UltiSnips_Manager._primary_filetype)"
endif
exec g:_uspy "vim.command(\"let file = '%s'\" % UltiSnips_Manager.file_to_edit(vim.eval(\"type\")))"
exec g:_uspy "vim.command(\"let file = '%s'\" % UltiSnips_Manager._file_to_edit(vim.eval(\"type\")))"
let mode = 'e'
if exists('g:UltiSnipsEditSplit')
@ -212,7 +212,7 @@ function! UltiSnips#SnippetsInCurrentScope()
endfunction
function! UltiSnips#SaveLastVisualSelection()
exec g:_uspy "UltiSnips_Manager.save_last_visual_selection()"
exec g:_uspy "UltiSnips_Manager._save_last_visual_selection()"
return ""
endfunction
@ -257,15 +257,15 @@ endfunction
function! UltiSnips#CursorMoved()
exec g:_uspy "UltiSnips_Manager.cursor_moved()"
exec g:_uspy "UltiSnips_Manager._cursor_moved()"
endf
function! UltiSnips#LeavingBuffer()
exec g:_uspy "UltiSnips_Manager.leaving_buffer()"
exec g:_uspy "UltiSnips_Manager._leaving_buffer()"
endf
function! UltiSnips#LeavingInsertMode()
exec g:_uspy "UltiSnips_Manager.leaving_insert_mode()"
exec g:_uspy "UltiSnips_Manager._leaving_insert_mode()"
endfunction
" }}}

View File

@ -346,11 +346,6 @@ file: >
" Traverse in reverse order
let g:UltiSnipsDontReverseSearchPath="0"
By default, whenever a snippet expand is triggered, UltiSnips will check for
modifications to the snippet file associated with the filetype and reload it
if necessary. This behavior can be disabled as follows: >
let g:UltiSnipsDoHash=0
|UltiSnips-adding-snippets| explains which files are parsed for a given filetype.

View File

@ -0,0 +1,10 @@
#!/usr/bin/env python
# encoding: utf-8
"""Sources of snippet definitions."""
# TODO(sirver): these should register themselves with the Manager, so that
# other plugins can extend them more easily.
from UltiSnips.providers.snippet_file import UltiSnipsFileProvider, \
base_snippet_files_for
from UltiSnips.providers.added_snippets_provider import AddedSnippetsProvider

View File

@ -0,0 +1,59 @@
#!/usr/bin/env python
# encoding: utf-8
"""Base class for snippet providers."""
from collections import defaultdict
from UltiSnips.providers._snippet_dictionary import SnippetDictionary
class SnippetProvider(object):
"""See module docstring."""
def __init__(self):
self._snippets = defaultdict(SnippetDictionary)
def get_snippets(self, filetypes, before, possible):
"""Returns the snippets for all 'filetypes' (in order) and their
parents matching the text 'before'. If 'possible' is true, a partial
match is enough."""
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 _find_snippets(self, ft, trigger, potentially=False, seen=None):
"""Find snippets matching 'trigger' for 'ft'. If 'potentially' is True,
partial matches are enough."""
snips = self._snippets.get(ft, None)
if not snips:
return []
if not seen:
seen = set()
seen.add(ft)
parent_results = []
# TODO(sirver): extends information is not bound to one
# provider. It should be tracked further up.
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)

View File

@ -6,14 +6,13 @@
import hashlib
import os
def _hash_file(path):
"""Returns a hashdigest of 'path'"""
if not os.path.isfile(path):
return False
return hashlib.sha1(open(path, "rb").read()).hexdigest()
# TODO(sirver): This class should not hash any files nor keep track of extends.
class SnippetDictionary(object):
"""See module docstring."""

View File

@ -0,0 +1,14 @@
#!/usr/bin/env python
# encoding: utf-8
"""Handles manually added snippets (i.e. not in a file)."""
from UltiSnips.providers._base import SnippetProvider
class AddedSnippetsProvider(SnippetProvider):
"""See module docstring."""
# TODO(sirver): filename makes no sense here. Is it even used?
def add_snippet(self, ft, snippet, filename):
"""Adds the given 'snippet' for 'ft'."""
self._snippets[ft].add_snippet(snippet, filename)

View File

@ -0,0 +1,182 @@
#!/usr/bin/env python
# encoding: utf-8
"""Code to provide access to UltiSnips files from disk."""
import glob
import os
from UltiSnips.providers._base import SnippetProvider
from UltiSnips.providers.ultisnips_file import parse_snippets_file
from UltiSnips.snippet_definition import SnippetDefinition
import UltiSnips._vim as _vim
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 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")
paths = _vim.eval("&runtimepath").split(',')
if _should_reverse_search_path():
paths = paths[::-1]
base_snippets = os.path.realpath(os.path.join(_plugin_dir(), "UltiSnips"))
ret = []
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
class SnippetSyntaxError(RuntimeError):
"""Thrown when a syntax error is found in a file."""
def __init__(self, filename, line_index, msg):
RuntimeError.__init__(self, "%s in %s:%d" % (
msg, filename, line_index))
class UltiSnipsFileProvider(SnippetProvider):
"""Manages all snippets definitons found in rtp."""
def get_snippets(self, filetypes, before, possible):
for ft in filetypes:
self._ensure_loaded(ft)
return SnippetProvider.get_snippets(self, filetypes, before, possible)
def _ensure_loaded(self, ft, already_loaded=None):
"""Make sure that the snippets for 'ft' and everything it extends are
loaded."""
if not already_loaded:
already_loaded = set()
if ft in already_loaded:
return
already_loaded.add(ft)
if self._needs_update(ft):
self._load_snippets_for(ft)
for parent in self._snippets[ft].extends:
self._ensure_loaded(parent, already_loaded)
def _needs_update(self, ft):
"""Returns true if any files for 'ft' have changed and must be
reloaded."""
if ft not in self._snippets:
return True
elif self._snippets[ft].has_any_file_changed():
return True
else:
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 _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 _parse_snippets(self, ft, filename):
"""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)
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))
raise SnippetSyntaxError(filename, line_index, msg)
elif event == "clearsnippets":
triggers, = data
self._snippets[ft].clear_snippets(triggers)
elif event == "extends":
# TODO(sirver): extends information is more global
# than one snippet provider.
filetypes, = data
self._add_extending_info(ft, filetypes)
elif event == "snippet":
trigger, value, description, options, globals = data
self._snippets[ft].add_snippet(
SnippetDefinition(trigger, value, description, options,
globals), filename
)
else:
assert False, "Unhandled %s: %r" % (event, data)
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)

View File

@ -33,7 +33,7 @@ def _words_for_line(trigger, before, num_words=None):
return before[len(before_words):].strip()
class Snippet(object):
class SnippetDefinition(object):
"""Represents a snippet as parsed from a file."""
_INDENT = re.compile(r"^[ \t]*")
@ -49,7 +49,7 @@ class Snippet(object):
self._globals = globals
def __repr__(self):
return "Snippet(%s,%s,%s)" % (
return "SnippetDefinition(%s,%s,%s)" % (
self._trigger, self._description, self._opts)
def _re_match(self, trigger):

View File

@ -5,17 +5,15 @@
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.providers import UltiSnipsFileProvider, \
base_snippet_files_for, AddedSnippetsProvider
from UltiSnips.snippet_definition import SnippetDefinition
from UltiSnips.vim_state import VimState, VisualContentPreserver
import UltiSnips._vim as _vim
@ -40,86 +38,6 @@ def _ask_snippets(snippets):
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
@ -137,13 +55,14 @@ 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 sends no WinLeave msg here.
self._leaving_buffer() # pylint:disable=protected-access
_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.
# TODO(sirver): This class is still too long. It should only contain public
# facing methods, most of the private methods should be moved outside of it.
class SnippetManager(object):
"""The main entry point for all UltiSnips functionality. All Vim functions
call methods in this class."""
@ -154,21 +73,7 @@ class SnippetManager(object):
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()
self._reset()
@err_to_scratch_buffer
def jump_forwards(self):
@ -188,12 +93,28 @@ class SnippetManager(object):
@err_to_scratch_buffer
def expand(self):
"""Trie to expand a snippet at the current position."""
"""Try 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 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 snippets_in_current_scope(self):
"""Returns the snippets that could be expanded to Vim as a global
@ -210,7 +131,7 @@ class SnippetManager(object):
key = as_unicode(snip.trigger)
description = as_unicode(description)
#remove surrounding "" or '' in snippet description if it exists
# remove surrounding "" or '' in snippet description if it exists
if len(description) > 2:
if (description[0] == description[-1] and
description[0] in "'\""):
@ -246,37 +167,12 @@ class SnippetManager(object):
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
self._added_snippets_provider.add_snippet(ft, SnippetDefinition(
trigger, value, description, options, globals or {}), fn
)
@err_to_scratch_buffer
@ -287,7 +183,7 @@ class SnippetManager(object):
globals = {}
before = _vim.buf.line_till_cursor
snip = Snippet(trigger, value, description, options, globals)
snip = SnippetDefinition(trigger, value, description, options, globals)
if not trigger or snip.matches(before):
self._do_snippet(snip, before)
@ -295,8 +191,28 @@ class SnippetManager(object):
else:
return False
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
@err_to_scratch_buffer
def cursor_moved(self):
def _cursor_moved(self):
"""Called whenever the cursor moved."""
self._vstate.remember_position()
if _vim.eval("mode()") not in 'in':
@ -360,40 +276,40 @@ class SnippetManager(object):
self._csnippets[0].update_textobjects()
self._vstate.remember_buffer(self._csnippets[0])
def leaving_buffer(self):
@err_to_scratch_buffer
def _reset(self):
"""Reset the class to the state it had directly after creation."""
self._vstate = VimState()
self._filetypes = defaultdict(lambda: ['all'])
self._visual_content = VisualContentPreserver()
self._snippet_providers = [
AddedSnippetsProvider(),
UltiSnipsFileProvider()
]
self._added_snippets_provider = self._snippet_providers[0]
while len(self._csnippets):
self._current_snippet_is_done()
self._reinit()
@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()
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
@ -440,7 +356,7 @@ class SnippetManager(object):
self._ignore_movements = True
return jumped
def leaving_insert_mode(self):
def _leaving_insert_mode(self):
"""Called whenever we leave the insert mode."""
self._vstate.restore_unnamed_register()
@ -484,28 +400,10 @@ class SnippetManager(object):
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]
snippets = []
for provider in self._snippet_providers:
snippets.extend(provider.get_snippets(filetypes, before, possible))
return snippets
def _do_snippet(self, snippet, before):
@ -574,39 +472,15 @@ class SnippetManager(object):
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):
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
# TODO(sirver): this should talk directly to the UltiSnipsFileProvider.
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.
@ -617,7 +491,7 @@ class SnippetManager(object):
# 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)
existing = base_snippet_files_for(ft, False)
filename = ft + ".snippets"
if _vim.eval("exists('g:UltiSnipsSnippetsDir')") == "1":
@ -644,95 +518,3 @@ class SnippetManager(object):
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)

181
test.py
View File

@ -31,16 +31,18 @@
# pylint: skip-file
import os
import tempfile
import unittest
import time
import re
import platform
import sys
import subprocess
from textwrap import dedent
import os
import platform
import random
import re
import shutil
import string
import subprocess
import sys
import tempfile
import time
import unittest
try:
import unidecode
@ -66,20 +68,21 @@ LS = "@" # List snippets
EX = "\t" # EXPAND
EA = "#" # Expand anonymous
# Some VIM functions
COMPL_KW = chr(24)+chr(14)
COMPL_ACCEPT = chr(25)
NUMBER_OF_RETRIES_FOR_EACH_TEST = 4
def RunningOnWindows():
def running_on_windows():
if platform.system() == "Windows":
return "Does not work on Windows."
def NoUnidecodeAvailable():
def no_unidecode_available():
if not UNIDECODE_IMPORTED:
return "unidecode is not available."
def random_string(n):
return ''.join(random.choice(string.ascii_lowercase) for x in range(n))
class VimInterface:
def focus(title=None):
@ -220,7 +223,7 @@ class VimInterfaceWindows(VimInterface):
class _VimTest(unittest.TestCase):
snippets = ("dummy", "donotdefine")
snippets_test_file = ("", "", "") # file type, file name, file content
snippets_test_file = ("", "") # filetype, file content
text_before = " --- some text before --- \n\n"
text_after = "\n\n --- some text after --- "
expected_error = ""
@ -256,7 +259,8 @@ class _VimTest(unittest.TestCase):
def check_output(self):
wanted = self.text_before + self.wanted + self.text_after
if self.expected_error:
wanted = wanted + "\n" + self.expected_error
self.assertRegexpMatches(self.output, self.expected_error)
return
for i in range(NUMBER_OF_RETRIES_FOR_EACH_TEST):
if self.output != wanted:
# Redo this, but slower
@ -273,6 +277,18 @@ class _VimTest(unittest.TestCase):
def _options_off(self):
pass
def _create_snippet_file(self, ft, content):
"""Create a snippet file and makes sure that it is found on the
runtimepath to be parsed."""
self._temporary_directory = tempfile.mkdtemp(prefix="UltiSnips_Test")
snippet_dir = random_string(20)
abs_snippet_dir = os.path.join(self._temporary_directory, snippet_dir)
os.mkdir(abs_snippet_dir)
with open(os.path.join(abs_snippet_dir, "%s.snippets" % ft), "w") as snippet_file:
snippet_file.write(dedent(content + "\n"))
self.vim.send(":let g:UltiSnipsSnippetDirectories=['%s']\n" % snippet_dir)
self.vim.send(""":set runtimepath=$VIMRUNTIME,%s,.\n""" % self._temporary_directory)
def setUp(self):
reason_for_skipping = self.skip_if()
if reason_for_skipping is not None:
@ -285,9 +301,9 @@ class _VimTest(unittest.TestCase):
self.send(":silent! close\n")
# Reset UltiSnips
self.send_py("UltiSnips_Manager.reset(test_error=True)")
self.send_py("UltiSnips_Manager._reset()")
# Make it unlikely that we do not parse any shipped snippets
# Make it unlikely that we do parse any shipped snippets.
self.send(":let g:UltiSnipsSnippetDirectories=['<un_def_ined>']\n")
# Clear the buffer
@ -308,10 +324,10 @@ class _VimTest(unittest.TestCase):
self.send_py("UltiSnips_Manager.add_snippet(%r, %r, %r, %r)" %
(sv, content, description, options))
ft, fn, file_data = self.snippets_test_file
ft, file_data = self.snippets_test_file
self._temporary_directory = ""
if ft:
self.send_py("UltiSnips_Manager._parse_snippets(%r, %r, %r)" %
(ft, fn, dedent(file_data + '\n')))
self._create_snippet_file(ft, file_data)
if not self.interrupt:
# Enter insert mode
@ -336,18 +352,17 @@ class _VimTest(unittest.TestCase):
self.output = self.vim.get_buffer_data()
def tearDown(self):
if self._temporary_directory:
self.vim.send(""":set runtimepath=$VIMRUNTIME,.\n""")
shutil.rmtree(self._temporary_directory)
###########################################################################
# BEGINNING OF TEST #
###########################################################################
# Snippet Definition Parsing {{{#
class _PS_Base(_VimTest):
def _options_on(self):
self.send(":let UltiSnipsDoHash=0\n")
def _options_off(self):
self.send(":unlet UltiSnipsDoHash\n")
class ParseSnippets_SimpleSnippet(_PS_Base):
snippets_test_file = ("all", "test_file", r"""
class ParseSnippets_SimpleSnippet(_VimTest):
snippets_test_file = ("all", r"""
snippet testsnip "Test Snippet" b!
This is a test snippet!
endsnippet
@ -355,39 +370,33 @@ class ParseSnippets_SimpleSnippet(_PS_Base):
keys = "testsnip" + EX
wanted = "This is a test snippet!"
class ParseSnippets_MissingEndSnippet(_PS_Base):
snippets_test_file = ("all", "test_file", r"""
class ParseSnippets_MissingEndSnippet(_VimTest):
snippets_test_file = ("all", r"""
snippet testsnip "Test Snippet" b!
This is a test snippet!
""")
keys = "testsnip" + EX
wanted = "testsnip" + EX
expected_error = dedent("""
UltiSnips: Missing 'endsnippet' for 'testsnip' in test_file(4)
""").strip()
expected_error = r"Missing 'endsnippet' for 'testsnip' in \S+:4"
class ParseSnippets_UnknownDirective(_PS_Base):
snippets_test_file = ("all", "test_file", r"""
class ParseSnippets_UnknownDirective(_VimTest):
snippets_test_file = ("all", r"""
unknown directive
""")
keys = "testsnip" + EX
wanted = "testsnip" + EX
expected_error = dedent("""
UltiSnips: Invalid line 'unknown directive' in test_file(2)
""").strip()
expected_error = r"Invalid line 'unknown directive' in \S+:2"
class ParseSnippets_ExtendsWithoutFiletype(_PS_Base):
snippets_test_file = ("all", "test_file", r"""
class ParseSnippets_ExtendsWithoutFiletype(_VimTest):
snippets_test_file = ("all", r"""
extends
""")
keys = "testsnip" + EX
wanted = "testsnip" + EX
expected_error = dedent("""
UltiSnips: 'extends' without file types in test_file(2)
""").strip()
expected_error = r"'extends' without file types in \S+:2"
class ParseSnippets_ClearAll(_PS_Base):
snippets_test_file = ("all", "test_file", r"""
class ParseSnippets_ClearAll(_VimTest):
snippets_test_file = ("all", r"""
snippet testsnip "Test snippet"
This is a test.
endsnippet
@ -397,8 +406,8 @@ class ParseSnippets_ClearAll(_PS_Base):
keys = "testsnip" + EX
wanted = "testsnip" + EX
class ParseSnippets_ClearOne(_PS_Base):
snippets_test_file = ("all", "test_file", r"""
class ParseSnippets_ClearOne(_VimTest):
snippets_test_file = ("all", r"""
snippet testsnip "Test snippet"
This is a test.
endsnippet
@ -412,8 +421,8 @@ class ParseSnippets_ClearOne(_PS_Base):
keys = "toclear" + EX + "\n" + "testsnip" + EX
wanted = "toclear" + EX + "\n" + "This is a test."
class ParseSnippets_ClearTwo(_PS_Base):
snippets_test_file = ("all", "test_file", r"""
class ParseSnippets_ClearTwo(_VimTest):
snippets_test_file = ("all", r"""
snippet testsnip "Test snippet"
This is a test.
endsnippet
@ -428,8 +437,8 @@ class ParseSnippets_ClearTwo(_PS_Base):
wanted = "toclear" + EX + "\n" + "testsnip" + EX
class _ParseSnippets_MultiWord(_PS_Base):
snippets_test_file = ("all", "test_file", r"""
class _ParseSnippets_MultiWord(_VimTest):
snippets_test_file = ("all", r"""
snippet /test snip/
This is a test.
endsnippet
@ -452,8 +461,8 @@ class ParseSnippets_MultiWord_Description_Option(_ParseSnippets_MultiWord):
keys = "snippet test" + EX
wanted = "This is yet another test."
class _ParseSnippets_MultiWord_RE(_PS_Base):
snippets_test_file = ("all", "test_file", r"""
class _ParseSnippets_MultiWord_RE(_VimTest):
snippets_test_file = ("all", r"""
snippet /[d-f]+/ "" r
az test
endsnippet
@ -476,16 +485,16 @@ class ParseSnippets_MultiWord_RE3(_ParseSnippets_MultiWord_RE):
keys = "test test test" + EX
wanted = "re-test"
class ParseSnippets_MultiWord_Quotes(_PS_Base):
snippets_test_file = ("all", "test_file", r"""
class ParseSnippets_MultiWord_Quotes(_VimTest):
snippets_test_file = ("all", r"""
snippet "test snip"
This is a test.
endsnippet
""")
keys = "test snip" + EX
wanted = "This is a test."
class ParseSnippets_MultiWord_WithQuotes(_PS_Base):
snippets_test_file = ("all", "test_file", r"""
class ParseSnippets_MultiWord_WithQuotes(_VimTest):
snippets_test_file = ("all", r"""
snippet !"test snip"!
This is a test.
endsnippet
@ -493,32 +502,28 @@ class ParseSnippets_MultiWord_WithQuotes(_PS_Base):
keys = '"test snip"' + EX
wanted = "This is a test."
class ParseSnippets_MultiWord_NoContainer(_PS_Base):
snippets_test_file = ("all", "test_file", r"""
class ParseSnippets_MultiWord_NoContainer(_VimTest):
snippets_test_file = ("all", r"""
snippet test snip
This is a test.
endsnippet
""")
keys = "test snip" + EX
wanted = keys
expected_error = dedent("""
UltiSnips: Invalid multiword trigger: 'test snip' in test_file(2)
""").strip()
expected_error = "Invalid multiword trigger: 'test snip' in \S+:2"
class ParseSnippets_MultiWord_UnmatchedContainer(_PS_Base):
snippets_test_file = ("all", "test_file", r"""
class ParseSnippets_MultiWord_UnmatchedContainer(_VimTest):
snippets_test_file = ("all", r"""
snippet !inv snip/
This is a test.
endsnippet
""")
keys = "inv snip" + EX
wanted = keys
expected_error = dedent("""
UltiSnips: Invalid multiword trigger: '!inv snip/' in test_file(2)
""").strip()
expected_error = "Invalid multiword trigger: '!inv snip/' in \S+:2"
class ParseSnippets_Global_Python(_PS_Base):
snippets_test_file = ("all", "test_file", r"""
class ParseSnippets_Global_Python(_VimTest):
snippets_test_file = ("all", r"""
global !p
def tex(ins):
return "a " + ins + " b"
@ -535,8 +540,8 @@ class ParseSnippets_Global_Python(_PS_Base):
keys = "ab" + EX + "\nac" + EX
wanted = "x a bob b y\nx a jon b y"
class ParseSnippets_Global_Local_Python(_PS_Base):
snippets_test_file = ("all", "test_file", r"""
class ParseSnippets_Global_Local_Python(_VimTest):
snippets_test_file = ("all", r"""
global !p
def tex(ins):
return "a " + ins + " b"
@ -855,43 +860,43 @@ class TabStopNavigatingInInsertModeSimple_ExceptCorrectResult(_VimTest):
# End: TabStop Tests #}}}
# ShellCode Interpolation {{{#
class TabStop_Shell_SimpleExample(_VimTest):
skip_if = lambda self: RunningOnWindows()
skip_if = lambda self: running_on_windows()
snippets = ("test", "hi `echo hallo` you!")
keys = "test" + EX + "and more"
wanted = "hi hallo you!and more"
class TabStop_Shell_WithUmlauts(_VimTest):
skip_if = lambda self: RunningOnWindows()
skip_if = lambda self: running_on_windows()
snippets = ("test", "hi `echo höüäh` you!")
keys = "test" + EX + "and more"
wanted = "hi höüäh you!and more"
class TabStop_Shell_TextInNextLine(_VimTest):
skip_if = lambda self: RunningOnWindows()
skip_if = lambda self: running_on_windows()
snippets = ("test", "hi `echo hallo`\nWeiter")
keys = "test" + EX + "and more"
wanted = "hi hallo\nWeiterand more"
class TabStop_Shell_InDefValue_Leave(_VimTest):
skip_if = lambda self: RunningOnWindows()
skip_if = lambda self: running_on_windows()
snippets = ("test", "Hallo ${1:now `echo fromecho`} end")
keys = "test" + EX + JF + "and more"
wanted = "Hallo now fromecho endand more"
class TabStop_Shell_InDefValue_Overwrite(_VimTest):
skip_if = lambda self: RunningOnWindows()
skip_if = lambda self: running_on_windows()
snippets = ("test", "Hallo ${1:now `echo fromecho`} end")
keys = "test" + EX + "overwrite" + JF + "and more"
wanted = "Hallo overwrite endand more"
class TabStop_Shell_TestEscapedChars_Overwrite(_VimTest):
skip_if = lambda self: RunningOnWindows()
skip_if = lambda self: running_on_windows()
snippets = ("test", r"""`echo \`echo "\\$hi"\``""")
keys = "test" + EX
wanted = "$hi"
class TabStop_Shell_TestEscapedCharsAndShellVars_Overwrite(_VimTest):
skip_if = lambda self: RunningOnWindows()
skip_if = lambda self: running_on_windows()
snippets = ("test", r"""`hi="blah"; echo \`echo "$hi"\``""")
keys = "test" + EX
wanted = "blah"
class TabStop_Shell_ShebangPython(_VimTest):
skip_if = lambda self: RunningOnWindows()
skip_if = lambda self: running_on_windows()
snippets = ("test", """Hallo ${1:now `#!/usr/bin/env python
print "Hallo Welt"
`} end""")
@ -1497,12 +1502,12 @@ class Transformation_CleverTransformLongLower_ExceptCorrectResult(_VimTest):
wanted = "HALLO hallo"
class Transformation_SimpleCaseAsciiResult(_VimTest):
skip_if = lambda self: NoUnidecodeAvailable()
skip_if = lambda self: no_unidecode_available()
snippets = ("ascii", "$1 ${1/(.*)/$1/a}")
keys = "ascii" + EX + "éèàçôïÉÈÀÇÔÏ€"
wanted = "éèàçôïÉÈÀÇÔÏ€ eeacoiEEACOIEU"
class Transformation_LowerCaseAsciiResult(_VimTest):
skip_if = lambda self: NoUnidecodeAvailable()
skip_if = lambda self: no_unidecode_available()
snippets = ("ascii", "$1 ${1/(.*)/\L$1\E/a}")
keys = "ascii" + EX + "éèàçôïÉÈÀÇÔÏ€"
wanted = "éèàçôïÉÈÀÇÔÏ€ eeacoieeacoieu"
@ -1909,9 +1914,9 @@ class RecTabStops_MirroredZeroTS_ECR(_VimTest):
keys = "m" + EX + "m1" + EX + "one" + JF + "two" + \
JF + "three" + JF + "four" + JF + "end"
wanted = "[ [ one three three two ] four ]end"
class RecTabStops_ChildTriggerContainsParentTextObjects(_PS_Base):
class RecTabStops_ChildTriggerContainsParentTextObjects(_VimTest):
# https://bugs.launchpad.net/ultisnips/+bug/1191617
snippets_test_file = ("all", "test_file", r"""
snippets_test_file = ("all", r"""
global !p
def complete(t, opts):
if t:
@ -2085,7 +2090,7 @@ class SnippetOptions_ExpandInwordSnippetsWithOtherChars_Expand2(_VimTest):
keys = "-test" + EX
wanted = "-Expand me!"
class SnippetOptions_ExpandInwordSnippetsWithOtherChars_Expand3(_VimTest):
skip_if = lambda self: RunningOnWindows()
skip_if = lambda self: running_on_windows()
snippets = (("test", "Expand me!", "", "i"), )
keys = "ßßtest" + EX
wanted = "ßßExpand me!"
@ -2409,7 +2414,7 @@ class RecTabStopsWithExpandtab_SpecialIndentProblem_ECR(_ExpandTabs):
# changes made 'manually', while the other vim version seem to do so. Since
# the fault is not with UltiSnips, we simply skip this test on windows
# completely.
skip_if = lambda self: RunningOnWindows()
skip_if = lambda self: running_on_windows()
snippets = (
("m1", "Something"),
("m", "\t$0"),
@ -2443,10 +2448,10 @@ class ProperIndenting_AutoIndentAndNewline_ECR(_VimTest):
def _options_off(self):
self.send(":set noautoindent\n")
# Test for bug 1073816
class ProperIndenting_FirstLineInFile_ECR(_PS_Base):
class ProperIndenting_FirstLineInFile_ECR(_VimTest):
text_before = ""
text_after = ""
snippets_test_file = ("all", "test_file", r"""
snippets_test_file = ("all", r"""
global !p
def complete(t, opts):
if t:
@ -2565,7 +2570,7 @@ hi4Hello"""
# Test for bug 871357 #
class TestLangmapWithUtf8_ExceptCorrectResult(_VimTest):
skip_if = lambda self: RunningOnWindows() # SendKeys can't send UTF characters
skip_if = lambda self: running_on_windows() # SendKeys can't send UTF characters
snippets = ("testme",
"""my snipped ${1:some_default}
and a mirror: $1
@ -2971,7 +2976,7 @@ class Snippet_With_DoubleQuote_List(_VimTest):
# End: Quotes in Snippets #}}}
# Umlauts and Special Chars {{{#
class _UmlautsBase(_VimTest):
skip_if = lambda self: RunningOnWindows() # SendKeys can't send UTF characters
skip_if = lambda self: running_on_windows() # SendKeys can't send UTF characters
class Snippet_With_Umlauts_List(_UmlautsBase):
snippets = _snip_quote('ü')