#!/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 '', self._actions['pre_expand'] if 'pre_expand' in self._actions else '', self._actions['post_expand'] if 'post_expand' in self._actions else '', 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