You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
215 lines
7.3 KiB
Python
215 lines
7.3 KiB
Python
#!/usr/bin/env python
|
|
# encoding: utf-8
|
|
|
|
"""Snippet representation after parsing."""
|
|
|
|
import re
|
|
|
|
from UltiSnips.compatibility import as_unicode
|
|
from UltiSnips.text_objects import SnippetInstance
|
|
from UltiSnips.indent_util import IndentUtil
|
|
import UltiSnips._vim as _vim
|
|
|
|
|
|
def _words_for_line(trigger, before, num_words=None):
|
|
""" Gets the final 'num_words' words from 'before'.
|
|
If num_words is None, then use the number of words in
|
|
'trigger'.
|
|
"""
|
|
if not len(before):
|
|
return ''
|
|
|
|
if num_words is None:
|
|
num_words = len(trigger.split())
|
|
|
|
word_list = before.split()
|
|
if len(word_list) <= num_words:
|
|
return before.strip()
|
|
else:
|
|
before_words = before
|
|
for i in range(-1, -(num_words + 1), -1):
|
|
left = before_words.rfind(word_list[i])
|
|
before_words = before_words[:left]
|
|
return before[len(before_words):].strip()
|
|
|
|
|
|
class SnippetDefinition(object):
|
|
"""Represents a snippet as parsed from a file."""
|
|
|
|
_INDENT = re.compile(r"^[ \t]*")
|
|
_TABS = re.compile(r"^\t*")
|
|
|
|
def __init__(self, priority, trigger, value, description, options, globals):
|
|
self._priority = priority
|
|
self._trigger = as_unicode(trigger)
|
|
self._value = as_unicode(value)
|
|
self._description = as_unicode(description)
|
|
self._opts = options
|
|
self._matched = ""
|
|
self._last_re = None
|
|
self._globals = globals
|
|
|
|
def __repr__(self):
|
|
return "SnippetDefinition(%r,%s,%s,%s)" % (
|
|
self._priority, self._trigger, self._description, self._opts)
|
|
|
|
def _re_match(self, trigger):
|
|
""" Test if a the current regex trigger matches
|
|
`trigger`. If so, set _last_re and _matched.
|
|
"""
|
|
for match in re.finditer(self._trigger, trigger):
|
|
if match.end() != len(trigger):
|
|
continue
|
|
else:
|
|
self._matched = trigger[match.start():match.end()]
|
|
|
|
self._last_re = match
|
|
return match
|
|
return False
|
|
|
|
def has_option(self, opt):
|
|
""" Check if the named option is set """
|
|
return opt in self._opts
|
|
|
|
def matches(self, trigger):
|
|
"""Returns True if this snippet matches 'trigger'."""
|
|
# If user supplies both "w" and "i", it should perhaps be an
|
|
# error, but if permitted it seems that "w" should take precedence
|
|
# (since matching at word boundary and within a word == matching at word
|
|
# boundary).
|
|
self._matched = ""
|
|
|
|
# Don't expand on whitespace
|
|
if trigger and trigger.rstrip() != trigger:
|
|
return False
|
|
|
|
words = _words_for_line(self._trigger, trigger)
|
|
|
|
if "r" in self._opts:
|
|
match = self._re_match(trigger)
|
|
elif "w" in self._opts:
|
|
words_len = len(self._trigger)
|
|
words_prefix = words[:-words_len]
|
|
words_suffix = words[-words_len:]
|
|
match = (words_suffix == self._trigger)
|
|
if match and words_prefix:
|
|
# Require a word boundary between prefix and suffix.
|
|
boundary_chars = words_prefix[-1:] + words_suffix[:1]
|
|
boundary_chars = boundary_chars.replace('"', '\\"')
|
|
match = _vim.eval('"%s" =~# "\\\\v.<."' % boundary_chars) != '0'
|
|
elif "i" in self._opts:
|
|
match = words.endswith(self._trigger)
|
|
else:
|
|
match = (words == self._trigger)
|
|
|
|
# By default, we match the whole trigger
|
|
if match and not self._matched:
|
|
self._matched = self._trigger
|
|
|
|
# Ensure the match was on a word boundry if needed
|
|
if "b" in self._opts and match:
|
|
text_before = trigger.rstrip()[:-len(self._matched)]
|
|
if text_before.strip(" \t") != '':
|
|
self._matched = ""
|
|
return False
|
|
return match
|
|
|
|
def could_match(self, trigger):
|
|
"""Return True if this snippet could match the (partial) 'trigger'."""
|
|
self._matched = ""
|
|
|
|
# List all on whitespace.
|
|
if trigger and trigger[-1] in (" ", "\t"):
|
|
trigger = ""
|
|
if trigger and trigger.rstrip() is not trigger:
|
|
return False
|
|
|
|
words = _words_for_line(self._trigger, trigger)
|
|
|
|
if "r" in self._opts:
|
|
# Test for full match only
|
|
match = self._re_match(trigger)
|
|
elif "w" in self._opts:
|
|
# Trim non-empty prefix up to word boundary, if present.
|
|
qwords = words.replace('"', '\\"')
|
|
words_suffix = _vim.eval(
|
|
'substitute("%s", "\\\\v^.+<(.+)", "\\\\1", "")' % qwords)
|
|
match = self._trigger.startswith(words_suffix)
|
|
self._matched = words_suffix
|
|
|
|
# TODO: list_snippets() function cannot handle partial-trigger
|
|
# matches yet, so for now fail if we trimmed the prefix.
|
|
if words_suffix != words:
|
|
match = False
|
|
elif "i" in self._opts:
|
|
# TODO: It is hard to define when a inword snippet could match,
|
|
# therefore we check only for full-word trigger.
|
|
match = self._trigger.startswith(words)
|
|
else:
|
|
match = self._trigger.startswith(words)
|
|
|
|
# By default, we match the words from the trigger
|
|
if match and not self._matched:
|
|
self._matched = words
|
|
|
|
# Ensure the match was on a word boundry if needed
|
|
if "b" in self._opts and match:
|
|
text_before = trigger.rstrip()[:-len(self._matched)]
|
|
if text_before.strip(" \t") != '':
|
|
self._matched = ""
|
|
return False
|
|
|
|
return match
|
|
|
|
@property
|
|
def description(self):
|
|
"""Descriptive text for this snippet."""
|
|
return ("(%s) %s" % (self._trigger, self._description)).strip()
|
|
|
|
@property
|
|
def priority(self):
|
|
"""The snippets priority, which defines which snippet will be preferred
|
|
over others with the same trigger."""
|
|
return self._priority
|
|
|
|
@property
|
|
def trigger(self):
|
|
"""The trigger text for the snippet."""
|
|
return self._trigger
|
|
|
|
@property
|
|
def matched(self):
|
|
"""The last text that matched this snippet in match() or
|
|
could_match()."""
|
|
return self._matched
|
|
|
|
def launch(self, text_before, visual_content, parent, start, end):
|
|
"""Launch this snippet, overwriting the text 'start' to 'end' and
|
|
keeping the 'text_before' on the launch line. 'Parent' is the parent
|
|
snippet instance if any."""
|
|
indent = self._INDENT.match(text_before).group(0)
|
|
lines = (self._value + "\n").splitlines()
|
|
ind_util = IndentUtil()
|
|
|
|
# Replace leading tabs in the snippet definition via proper indenting
|
|
snippet_definition = []
|
|
for line_num, line in enumerate(lines):
|
|
if "t" in self._opts:
|
|
tabs = 0
|
|
else:
|
|
tabs = len(self._TABS.match(line).group(0))
|
|
|
|
line_ind = ind_util.ntabs_to_proper_indent(tabs)
|
|
|
|
if line_num != 0:
|
|
line_ind = indent + line_ind
|
|
|
|
snippet_definition.append(line_ind + line[tabs:])
|
|
snippet_definition = '\n'.join(snippet_definition)
|
|
|
|
si = SnippetInstance(self, parent, indent, snippet_definition, start,
|
|
end, visual_content, last_re=self._last_re,
|
|
globals=self._globals)
|
|
|
|
return si
|