444 lines
14 KiB
Python
444 lines
14 KiB
Python
#!/usr/bin/env python
|
|
# encoding: utf-8
|
|
|
|
"""Snippet representation after parsing."""
|
|
|
|
import re
|
|
|
|
import vim
|
|
import textwrap
|
|
|
|
from UltiSnips import _vim
|
|
from UltiSnips.compatibility import as_unicode
|
|
from UltiSnips.indent_util import IndentUtil
|
|
from UltiSnips.text import escape
|
|
from UltiSnips.text_objects import SnippetInstance
|
|
from UltiSnips.text_objects._python_code import \
|
|
SnippetUtilCursor, SnippetUtilForAction
|
|
|
|
__WHITESPACE_SPLIT = re.compile(r"\s")
|
|
|
|
|
|
def split_at_whitespace(string):
|
|
"""Like string.split(), but keeps empty words as empty words."""
|
|
return re.split(__WHITESPACE_SPLIT, string)
|
|
|
|
|
|
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 num_words is None:
|
|
num_words = len(split_at_whitespace(trigger))
|
|
|
|
word_list = split_at_whitespace(before)
|
|
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, location, context, actions):
|
|
self._priority = int(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
|
|
self._location = location
|
|
self._context_code = context
|
|
self._context = None
|
|
self._actions = actions
|
|
|
|
# Make sure that we actually match our trigger in case we are
|
|
# immediately expanded.
|
|
self.matches(self._trigger)
|
|
|
|
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 _context_match(self, visual_content):
|
|
# skip on empty buffer
|
|
if len(vim.current.buffer) == 1 and vim.current.buffer[0] == "":
|
|
return
|
|
|
|
locals = {
|
|
'context': None,
|
|
'visual_mode': '',
|
|
'visual_text': '',
|
|
'last_placeholder': None
|
|
}
|
|
|
|
if visual_content:
|
|
locals['visual_mode'] = visual_content.mode
|
|
locals['visual_text'] = visual_content.text
|
|
locals['last_placeholder'] = visual_content.placeholder
|
|
|
|
return self._eval_code('snip.context = ' + self._context_code,
|
|
locals).context
|
|
|
|
def _eval_code(self, code, additional_locals={}):
|
|
code = "\n".join([
|
|
'import re, os, vim, string, random',
|
|
'\n'.join(self._globals.get('!p', [])).replace('\r\n', '\n'),
|
|
code
|
|
])
|
|
|
|
current = vim.current
|
|
|
|
locals = {
|
|
'window': current.window,
|
|
'buffer': current.buffer,
|
|
'line': current.window.cursor[0]-1,
|
|
'column': current.window.cursor[1]-1,
|
|
'cursor': SnippetUtilCursor(current.window.cursor),
|
|
}
|
|
|
|
locals.update(additional_locals)
|
|
|
|
snip = SnippetUtilForAction(locals)
|
|
|
|
try:
|
|
exec(code, {'snip': snip})
|
|
except Exception as e:
|
|
self._make_debug_exception(e, code)
|
|
raise
|
|
|
|
return snip
|
|
|
|
def _execute_action(
|
|
self,
|
|
action,
|
|
context,
|
|
additional_locals={}
|
|
):
|
|
mark_to_use = '`'
|
|
with _vim.save_mark(mark_to_use):
|
|
_vim.set_mark_from_pos(mark_to_use, _vim.get_cursor_pos())
|
|
|
|
cursor_line_before = _vim.buf.line_till_cursor
|
|
|
|
locals = {
|
|
'context': context,
|
|
}
|
|
|
|
locals.update(additional_locals)
|
|
|
|
snip = self._eval_code(action, locals)
|
|
|
|
if snip.cursor.is_set():
|
|
vim.current.window.cursor = snip.cursor.to_vim_cursor()
|
|
else:
|
|
new_mark_pos = _vim.get_mark_pos(mark_to_use)
|
|
|
|
cursor_invalid = False
|
|
|
|
if _vim._is_pos_zero(new_mark_pos):
|
|
cursor_invalid = True
|
|
else:
|
|
_vim.set_cursor_from_pos(new_mark_pos)
|
|
if cursor_line_before != _vim.buf.line_till_cursor:
|
|
cursor_invalid = True
|
|
|
|
if cursor_invalid:
|
|
raise RuntimeError(
|
|
'line under the cursor was modified, but ' +
|
|
'"snip.cursor" variable is not set; either set set ' +
|
|
'"snip.cursor" to new cursor position, or do not ' +
|
|
'modify cursor line'
|
|
)
|
|
|
|
return snip
|
|
|
|
def _make_debug_exception(self, e, code=''):
|
|
e.snippet_info = textwrap.dedent("""
|
|
Defined in: {}
|
|
Trigger: {}
|
|
Description: {}
|
|
Context: {}
|
|
Pre-expand: {}
|
|
Post-expand: {}
|
|
""").format(
|
|
self._location,
|
|
self._trigger,
|
|
self._description,
|
|
self._context_code if self._context_code else '<none>',
|
|
self._actions['pre_expand'] if 'pre_expand' in self._actions
|
|
else '<none>',
|
|
self._actions['post_expand'] if 'post_expand' in self._actions
|
|
else '<none>',
|
|
code,
|
|
)
|
|
|
|
e.snippet_code = code
|
|
|
|
def has_option(self, opt):
|
|
"""Check if the named option is set."""
|
|
return opt in self._opts
|
|
|
|
@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
|
|
|
|
@property
|
|
def location(self):
|
|
"""Where this snippet was defined."""
|
|
return self._location
|
|
|
|
@property
|
|
def context(self):
|
|
"""The matched context."""
|
|
return self._context
|
|
|
|
def matches(self, before, visual_content=None):
|
|
"""Returns True if this snippet matches 'before'."""
|
|
# 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 = ''
|
|
|
|
words = _words_for_line(self._trigger, before)
|
|
|
|
if 'r' in self._opts:
|
|
try:
|
|
match = self._re_match(before)
|
|
except Exception as e:
|
|
self._make_debug_exception(e)
|
|
raise
|
|
|
|
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 = escape(words_prefix[-1:] +
|
|
words_suffix[:1], r'\"')
|
|
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 = before.rstrip()[:-len(self._matched)]
|
|
if text_before.strip(' \t') != '':
|
|
self._matched = ''
|
|
return False
|
|
|
|
self._context = None
|
|
if match and self._context_code:
|
|
self._context = self._context_match(visual_content)
|
|
if not self.context:
|
|
match = False
|
|
|
|
return match
|
|
|
|
def could_match(self, before):
|
|
"""Return True if this snippet could match the (partial) 'before'."""
|
|
self._matched = ''
|
|
|
|
# List all on whitespace.
|
|
if before and before[-1] in (' ', '\t'):
|
|
before = ''
|
|
if before and before.rstrip() is not before:
|
|
return False
|
|
|
|
words = _words_for_line(self._trigger, before)
|
|
|
|
if 'r' in self._opts:
|
|
# Test for full match only
|
|
match = self._re_match(before)
|
|
elif 'w' in self._opts:
|
|
# Trim non-empty prefix up to word boundary, if present.
|
|
qwords = escape(words, r'\"')
|
|
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 = before.rstrip()[:-len(self._matched)]
|
|
if text_before.strip(' \t') != '':
|
|
self._matched = ''
|
|
return False
|
|
|
|
return match
|
|
|
|
def instantiate(self, snippet_instance, initial_text, indent):
|
|
"""Parses the content of this snippet and brings the corresponding text
|
|
objects alive inside of Vim."""
|
|
raise NotImplementedError()
|
|
|
|
def do_pre_expand(self, visual_content, snippets_stack):
|
|
if 'pre_expand' in self._actions:
|
|
locals = {'buffer': _vim.buf, 'visual_content': visual_content}
|
|
|
|
snip = self._execute_action(
|
|
self._actions['pre_expand'], self._context, locals
|
|
)
|
|
|
|
self._context = snip.context
|
|
|
|
return snip.cursor.is_set()
|
|
else:
|
|
return False
|
|
|
|
def do_post_expand(self, start, end, snippets_stack):
|
|
if 'post_expand' in self._actions:
|
|
locals = {
|
|
'snippet_start': start,
|
|
'snippet_end': end,
|
|
'buffer': _vim.buf
|
|
}
|
|
|
|
snip = self._execute_action(
|
|
self._actions['post_expand'], snippets_stack[-1].context, locals
|
|
)
|
|
|
|
snippets_stack[-1].context = snip.context
|
|
|
|
return snip.cursor.is_set()
|
|
else:
|
|
return False
|
|
|
|
def do_post_jump(
|
|
self, tabstop_number, jump_direction, snippets_stack, current_snippet
|
|
):
|
|
if 'post_jump' in self._actions:
|
|
start = current_snippet.start
|
|
end = current_snippet.end
|
|
|
|
locals = {
|
|
'tabstop': tabstop_number,
|
|
'jump_direction': jump_direction,
|
|
'tabstops': current_snippet.get_tabstops(),
|
|
'snippet_start': start,
|
|
'snippet_end': end,
|
|
'buffer': _vim.buf
|
|
}
|
|
|
|
snip = self._execute_action(
|
|
self._actions['post_jump'], current_snippet.context, locals
|
|
)
|
|
|
|
current_snippet.context = snip.context
|
|
|
|
return snip.cursor.is_set()
|
|
else:
|
|
return False
|
|
|
|
|
|
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
|
|
initial_text = []
|
|
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
|
|
|
|
result_line = line_ind + line[tabs:]
|
|
if 'm' in self._opts:
|
|
result_line = result_line.rstrip()
|
|
initial_text.append(result_line)
|
|
initial_text = '\n'.join(initial_text)
|
|
|
|
snippet_instance = SnippetInstance(
|
|
self, parent, initial_text, start, end, visual_content,
|
|
last_re=self._last_re, globals=self._globals,
|
|
context=self._context)
|
|
self.instantiate(snippet_instance, initial_text, indent)
|
|
|
|
snippet_instance.update_textobjects()
|
|
return snippet_instance
|