From 47479ea802b1c01865f87d320e9d5397c9c18790 Mon Sep 17 00:00:00 2001 From: Holger Rapp Date: Wed, 5 Feb 2014 20:58:40 +0100 Subject: [PATCH] Done with simplifying __init__.py. --- plugin/UltiSnips/__init__.py | 192 +--------------------- plugin/UltiSnips/snippet.py | 199 +++++++++++++++++++++++ plugin/UltiSnips/snippets_file_parser.py | 132 +++++++++++++++ 3 files changed, 333 insertions(+), 190 deletions(-) create mode 100755 plugin/UltiSnips/snippet.py create mode 100644 plugin/UltiSnips/snippets_file_parser.py diff --git a/plugin/UltiSnips/__init__.py b/plugin/UltiSnips/__init__.py index a662819..136753c 100755 --- a/plugin/UltiSnips/__init__.py +++ b/plugin/UltiSnips/__init__.py @@ -1,8 +1,8 @@ #!/usr/bin/env python # encoding: utf-8 -from functools import wraps from collections import defaultdict +from functools import wraps import glob import os import re @@ -11,10 +11,9 @@ import traceback from UltiSnips._diff import diff, guess_edit from UltiSnips.compatibility import as_unicode from UltiSnips.geometry import Position +from UltiSnips.snippet import Snippet from UltiSnips.snippet_dictionary import SnippetDictionary from UltiSnips.snippets_file_parser import SnippetsFileParser -from UltiSnips.text_objects import SnippetInstance -from UltiSnips.util import IndentUtil from UltiSnips.vim_state import VimState from UltiSnips.visual_content_preserver import VisualContentPreserver import UltiSnips._vim as _vim @@ -78,28 +77,6 @@ def _base_snippet_files_for(ft, default=True): return ret -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'. - """ - words = '' - 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() - def _plugin_dir(): """ Calculates the plugin directory for UltiSnips. This depends on the current file being 3 levels deep from the plugin directory, so it needs to @@ -160,171 +137,6 @@ Following is the full stack trace: _vim.new_scratch_buffer(s) return wrapper -class Snippet(object): - _INDENT = re.compile(r"^[ \t]*") - _TABS = re.compile(r"^\t*") - - def __init__(self, trigger, value, descr, options, globals): - self._t = as_unicode(trigger) - self._v = as_unicode(value) - self._d = as_unicode(descr) - self._opts = options - self._matched = "" - self._last_re = None - self._globals = globals - - def __repr__(self): - return "Snippet(%s,%s,%s)" % (self._t, self._d, 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._t, 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): - # 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._t, trigger) - - if "r" in self._opts: - match = self._re_match(trigger) - elif "w" in self._opts: - words_len = len(self._t) - words_prefix = words[:-words_len] - words_suffix = words[-words_len:] - match = (words_suffix == self._t) - if match and words_prefix: - # Require a word boundary between prefix and suffix. - boundaryChars = words_prefix[-1:] + words_suffix[:1] - boundaryChars = boundaryChars.replace('"', '\\"') - match = _vim.eval('"%s" =~# "\\\\v.<."' % boundaryChars) != '0' - elif "i" in self._opts: - match = words.endswith(self._t) - else: - match = (words == self._t) - - # By default, we match the whole trigger - if match and not self._matched: - self._matched = self._t - - # 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): - 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._t, 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._t.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._t.startswith(words) - else: - match = self._t.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 overwrites_previous(self): - return "!" in self._opts - - @property - def description(self): - return ("(%s) %s" % (self._t, self._d)).strip() - - @property - def trigger(self): - return self._t - - @property - def matched(self): - """ The last text that was matched. """ - return self._matched - - def launch(self, text_before, visual_content, parent, start, end): - indent = self._INDENT.match(text_before).group(0) - lines = (self._v + "\n").splitlines() - ind_util = IndentUtil() - - # Replace leading tabs in the snippet definition via proper indenting - v = [] - 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 - - v.append(line_ind + line[tabs:]) - v = '\n'.join(v) - - si = SnippetInstance(self, parent, indent, v, start, end, visual_content, - last_re = self._last_re, globals = self._globals) - - return si - class SnippetManager(object): def __init__(self): self._supertab_keys = None diff --git a/plugin/UltiSnips/snippet.py b/plugin/UltiSnips/snippet.py new file mode 100755 index 0000000..ff64299 --- /dev/null +++ b/plugin/UltiSnips/snippet.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python +# encoding: utf-8 + +import re + +from UltiSnips.compatibility import as_unicode +from UltiSnips.text_objects import SnippetInstance +from UltiSnips.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'. + """ + words = '' + 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 Snippet(object): + _INDENT = re.compile(r"^[ \t]*") + _TABS = re.compile(r"^\t*") + + def __init__(self, trigger, value, descr, options, globals): + """Represents a snippet as parsed from a file.""" + self._t = as_unicode(trigger) + self._v = as_unicode(value) + self._d = as_unicode(descr) + self._opts = options + self._matched = "" + self._last_re = None + self._globals = globals + + def __repr__(self): + return "Snippet(%s,%s,%s)" % (self._t, self._d, 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._t, 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): + # 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._t, trigger) + + if "r" in self._opts: + match = self._re_match(trigger) + elif "w" in self._opts: + words_len = len(self._t) + words_prefix = words[:-words_len] + words_suffix = words[-words_len:] + match = (words_suffix == self._t) + if match and words_prefix: + # Require a word boundary between prefix and suffix. + boundaryChars = words_prefix[-1:] + words_suffix[:1] + boundaryChars = boundaryChars.replace('"', '\\"') + match = _vim.eval('"%s" =~# "\\\\v.<."' % boundaryChars) != '0' + elif "i" in self._opts: + match = words.endswith(self._t) + else: + match = (words == self._t) + + # By default, we match the whole trigger + if match and not self._matched: + self._matched = self._t + + # 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): + 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._t, 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._t.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._t.startswith(words) + else: + match = self._t.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 overwrites_previous(self): + return "!" in self._opts + + @property + def description(self): + return ("(%s) %s" % (self._t, self._d)).strip() + + @property + def trigger(self): + return self._t + + @property + def matched(self): + """ The last text that was matched. """ + return self._matched + + def launch(self, text_before, visual_content, parent, start, end): + indent = self._INDENT.match(text_before).group(0) + lines = (self._v + "\n").splitlines() + ind_util = IndentUtil() + + # Replace leading tabs in the snippet definition via proper indenting + v = [] + 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 + + v.append(line_ind + line[tabs:]) + v = '\n'.join(v) + + si = SnippetInstance(self, parent, indent, v, start, end, visual_content, + last_re = self._last_re, globals = self._globals) + + return si diff --git a/plugin/UltiSnips/snippets_file_parser.py b/plugin/UltiSnips/snippets_file_parser.py new file mode 100644 index 0000000..31f2224 --- /dev/null +++ b/plugin/UltiSnips/snippets_file_parser.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python +# encoding: utf-8 + +import re +import UltiSnips._vim as _vim + +# TODO(sirver): This could just as well be a function. +class SnippetsFileParser(object): + def __init__(self, ft, fn, snip_manager, file_data=None): + """Parser 'fn' as filetype 'ft'.""" + self._sm = snip_manager + self._ft = ft + self._fn = fn + self._globals = {} + if file_data is None: + self._lines = open(fn).readlines() + else: + self._lines = file_data.splitlines(True) + self._idx = 0 + + def _error(self, msg): + fn = _vim.eval("""fnamemodify(%s, ":~:.")""" % _vim.escape(self._fn)) + self._sm._error("%s in %s(%d)" % (msg, fn, self._idx + 1)) + + def _line(self): + if self._idx < len(self._lines): + line = self._lines[self._idx] + else: + line = "" + return line + + def _line_head_tail(self): + parts = re.split(r"\s+", self._line().rstrip(), maxsplit=1) + parts.append('') + return parts[:2] + + def _line_head(self): + return self._line_head_tail()[0] + + def _line_tail(self): + return self._line_head_tail()[1] + + def _goto_next_line(self): + self._idx += 1 + return self._line() + + def _parse_first(self, line): + """ Parses the first line of the snippet definition. Returns the + snippet type, trigger, description, and options in a tuple in that + order. + """ + cdescr = "" + coptions = "" + cs = "" + + # Ensure this is a snippet + snip = line.split()[0] + + # Get and strip options if they exist + remain = line[len(snip):].strip() + words = remain.split() + if len(words) > 2: + # second to last word ends with a quote + if '"' not in words[-1] and words[-2][-1] == '"': + coptions = words[-1] + remain = remain[:-len(coptions) - 1].rstrip() + + # Get and strip description if it exists + remain = remain.strip() + if len(remain.split()) > 1 and remain[-1] == '"': + left = remain[:-1].rfind('"') + if left != -1 and left != 0: + cdescr, remain = remain[left:], remain[:left] + + # The rest is the trigger + cs = remain.strip() + if len(cs.split()) > 1 or "r" in coptions: + if cs[0] != cs[-1]: + self._error("Invalid multiword trigger: '%s'" % cs) + cs = "" + else: + cs = cs[1:-1] + + return (snip, cs, cdescr, coptions) + + def _parse_snippet(self): + line = self._line() + + (snip, trig, desc, opts) = self._parse_first(line) + end = "end" + snip + cv = "" + + while self._goto_next_line(): + line = self._line() + if line.rstrip() == end: + cv = cv[:-1] # Chop the last newline + break + cv += line + else: + self._error("Missing 'endsnippet' for %r" % trig) + return None + + if not trig: + # there was an error + return None + elif snip == "global": + # add snippet contents to file globals + if trig not in self._globals: + self._globals[trig] = [] + self._globals[trig].append(cv) + elif snip == "snippet": + self._sm.add_snippet(trig, cv, desc, opts, self._ft, self._globals, fn=self._fn) + else: + self._error("Invalid snippet type: '%s'" % snip) + + def parse(self): + while self._line(): + head, tail = self._line_head_tail() + if head == "extends": + if tail: + self._sm.add_extending_info(self._ft, + [ p.strip() for p in tail.split(',') ]) + else: + self._error("'extends' without file types") + elif head in ("snippet", "global"): + self._parse_snippet() + elif head == "clearsnippets": + self._sm.clear_snippets(tail.split(), self._ft) + elif head and not head.startswith('#'): + self._error("Invalid line %r" % self._line().rstrip()) + break + self._goto_next_line()