From 0c63f205aa3f477851e3d237f23cc2af1b6591ad Mon Sep 17 00:00:00 2001 From: Holger Rapp Date: Wed, 11 Jan 2012 19:17:13 +0100 Subject: [PATCH] Added basic support for the token. No documentation yet, but 16 new tests --- plugin/UltiSnips.vim | 10 +++++ plugin/UltiSnips/Compatibility.py | 8 ++-- plugin/UltiSnips/Lexer.py | 43 ++++++++++++++++--- plugin/UltiSnips/TextObjects.py | 38 +++++++++++++++-- plugin/UltiSnips/Util.py | 6 +++ plugin/UltiSnips/__init__.py | 50 +++++++++++++++------- test.py | 70 +++++++++++++++++++++++++++++++ 7 files changed, 198 insertions(+), 27 deletions(-) diff --git a/plugin/UltiSnips.vim b/plugin/UltiSnips.vim index 4da889d..49d7561 100644 --- a/plugin/UltiSnips.vim +++ b/plugin/UltiSnips.vim @@ -144,6 +144,15 @@ function! UltiSnips_ListSnippets() return "" endfunction +function! UltiSnips_SaveLastVisualSelection() + if has("python3") + python3 UltiSnips_Manager.save_last_visual_selection() + else + python UltiSnips_Manager.save_last_visual_selection() + endif + return "" +endfunction + function! UltiSnips_JumpBackwards() call CompensateForPUM() if has("python3") @@ -221,6 +230,7 @@ function! UltiSnips_MapKeys() exec "inoremap " . g:UltiSnipsJumpForwardTrigger . " =UltiSnips_JumpForwards()" exec "snoremap " . g:UltiSnipsJumpForwardTrigger . " :call UltiSnips_JumpForwards()" endif + exec 'xnoremap ' . g:UltiSnipsExpandTrigger. ' :call UltiSnips_SaveLastVisualSelection()gvs' exec "inoremap " . g:UltiSnipsJumpBackwardTrigger . " =UltiSnips_JumpBackwards()" exec "snoremap " . g:UltiSnipsJumpBackwardTrigger . " :call UltiSnips_JumpBackwards()" exec "inoremap " . g:UltiSnipsListSnippets . " =UltiSnips_ListSnippets()" diff --git a/plugin/UltiSnips/Compatibility.py b/plugin/UltiSnips/Compatibility.py index 143e174..eeab008 100644 --- a/plugin/UltiSnips/Compatibility.py +++ b/plugin/UltiSnips/Compatibility.py @@ -15,7 +15,7 @@ __all__ = ['as_unicode', 'compatible_exec', 'CheapTotalOrdering', 'vim_cursor', if sys.version_info >= (3,0): from UltiSnips.Compatibility_py3 import * - def set_vim_cursor(line, col): # TODO + def set_vim_cursor(line, col): """Wrapper around vims access to window.cursor. It can't handle multibyte chars, we therefore have to compensate""" @@ -24,7 +24,7 @@ if sys.version_info >= (3,0): vim.current.window.cursor = line, nbytes - def vim_cursor(): # TODO + def vim_cursor(): """Returns the character position (not the byte position) of the vim cursor""" @@ -60,7 +60,7 @@ if sys.version_info >= (3,0): else: from UltiSnips.Compatibility_py2 import * - def set_vim_cursor(line, col): # TODO + def set_vim_cursor(line, col): """Wrapper around vims access to window.cursor. It can't handle multibyte chars, we therefore have to compensate""" @@ -69,7 +69,7 @@ else: vim.current.window.cursor = line, nbytes - def vim_cursor(): # TODO + def vim_cursor(): """Returns the character position (not the byte position) of the vim cursor""" diff --git a/plugin/UltiSnips/Lexer.py b/plugin/UltiSnips/Lexer.py index b18e2a4..c3092ae 100644 --- a/plugin/UltiSnips/Lexer.py +++ b/plugin/UltiSnips/Lexer.py @@ -12,7 +12,7 @@ import re from UltiSnips.Geometry import Position __all__ = [ - "tokenize", "EscapeCharToken", "TransformationToken", "TabStopToken", + "tokenize", "EscapeCharToken", "VisualToken", "TransformationToken", "TabStopToken", "MirrorToken", "PythonCodeToken", "VimLCodeToken", "ShellCodeToken" ] @@ -43,8 +43,10 @@ class _TextIterator(object): return rv def peek(self, count = 1): - try: + if count > 1: # This might return '' if nothing is found return self._text[self._idx:self._idx + count] + try: + return self._text[self._idx] except IndexError: return None @@ -114,7 +116,7 @@ class TabStopToken(Token): @classmethod def starts_here(klass, stream): - return klass.CHECK.match(stream.peek(10)) != None + return klass.CHECK.match(stream.peek(10)) is not None def _parse(self, stream, indent): stream.next() # $ @@ -131,12 +133,41 @@ class TabStopToken(Token): self.start, self.end, self.no, self.initial_text ) +class VisualToken(Token): + TOKEN = "${VISUAL}" + CHECK = re.compile(r"^[ \t]*\${VISUAL}") + + @classmethod + def starts_here(klass, stream): + return klass.CHECK.match(stream.peek(10000)) is not None + + def _parse(self, stream, indent): + self.leading_whitespace = "" + while stream.peek() != self.TOKEN[0]: + self.leading_whitespace += stream.next() + + for i in range(len(self.TOKEN)): + stream.next() + + # Make sure that a ${VISUAL} at the end of a line behaves like a block + # of text and does not introduce another line break. + while 1: + nc = stream.peek() + if nc is None or nc not in '\r\n': + break + stream.next() + + def __repr__(self): + return "VisualToken(%r,%r)" % ( + self.start, self.end + ) + class TransformationToken(Token): CHECK = re.compile(r'^\${\d+\/') @classmethod def starts_here(klass, stream): - return klass.CHECK.match(stream.peek(10)) != None + return klass.CHECK.match(stream.peek(10)) is not None def _parse(self, stream, indent): stream.next() # $ @@ -160,7 +191,7 @@ class MirrorToken(Token): @classmethod def starts_here(klass, stream): - return klass.CHECK.match(stream.peek(10)) != None + return klass.CHECK.match(stream.peek(10)) is not None def _parse(self, stream, indent): stream.next() # $ @@ -250,7 +281,7 @@ class VimLCodeToken(Token): # End: Tokens }}} __ALLOWED_TOKENS = [ - EscapeCharToken, TransformationToken, TabStopToken, MirrorToken, + EscapeCharToken, VisualToken, TransformationToken, TabStopToken, MirrorToken, PythonCodeToken, VimLCodeToken, ShellCodeToken ] def tokenize(text, indent): diff --git a/plugin/UltiSnips/TextObjects.py b/plugin/UltiSnips/TextObjects.py index 3fa56b2..d6476af 100644 --- a/plugin/UltiSnips/TextObjects.py +++ b/plugin/UltiSnips/TextObjects.py @@ -11,8 +11,9 @@ from UltiSnips.Buffer import TextBuffer from UltiSnips.Compatibility import CheapTotalOrdering from UltiSnips.Compatibility import compatible_exec, as_unicode from UltiSnips.Geometry import Span, Position -from UltiSnips.Lexer import tokenize, EscapeCharToken, TransformationToken, \ - TabStopToken, MirrorToken, PythonCodeToken, VimLCodeToken, ShellCodeToken +from UltiSnips.Lexer import tokenize, EscapeCharToken, VisualToken, \ + TransformationToken, TabStopToken, MirrorToken, PythonCodeToken, \ + VimLCodeToken, ShellCodeToken from UltiSnips.Util import IndentUtil __all__ = [ "Mirror", "Transformation", "SnippetInstance", "StartMarker" ] @@ -166,6 +167,8 @@ class _TOParser(object): k._do_parse(all_tokens, seen_ts) elif isinstance(token, EscapeCharToken): EscapedChar(self._parent_to, token) + elif isinstance(token, VisualToken): + Visual(self._parent_to, token) elif isinstance(token, ShellCodeToken): ShellCode(self._parent_to, token) elif isinstance(token, PythonCodeToken): @@ -420,6 +423,34 @@ class Mirror(TextObject): def __repr__(self): return "Mirror(%s -> %s)" % (self._start, self._end) +class Visual(TextObject): + """ + A ${VISUAL} placeholder that will use the text that was last visually + selected and insert it here. If there was no text visually selected, + this will be the empty string + """ + def __init__(self, parent, token): + + # Find our containing snippet for visual_content + snippet = parent + while snippet and not isinstance(snippet, SnippetInstance): + snippet = snippet._parent + + text = "" + for idx, line in enumerate(snippet.visual_content.splitlines(True)): + text += token.leading_whitespace + text += line + + self._text = text + + TextObject.__init__(self, parent, token, initial_text = self._text) + + def _do_update(self): + self.current_text = self._text + + def __repr__(self): + return "Visual(%s -> %s)" % (self._start, self._end) + class Transformation(Mirror): def __init__(self, parent, ts, token): @@ -715,7 +746,7 @@ class SnippetInstance(TextObject): also a TextObject because it has a start an end """ - def __init__(self, parent, indent, initial_text, start = None, end = None, last_re = None, globals = None): + def __init__(self, parent, indent, initial_text, start, end, visual_content, last_re, globals): if start is None: start = Position(0,0) if end is None: @@ -723,6 +754,7 @@ class SnippetInstance(TextObject): self.locals = {"match" : last_re} self.globals = globals + self.visual_content = visual_content TextObject.__init__(self, parent, start, end, initial_text) diff --git a/plugin/UltiSnips/Util.py b/plugin/UltiSnips/Util.py index 405a8f7..7beeb6c 100644 --- a/plugin/UltiSnips/Util.py +++ b/plugin/UltiSnips/Util.py @@ -48,6 +48,12 @@ class IndentUtil(object): new_ind.append(ch) return "".join(new_ind) + def ntabs_to_proper_indent(self, ntabs): + line_ind = ntabs * self.sw * " " + line_ind = self.indent_to_spaces(line_ind) + line_ind = self.spaces_to_indent(line_ind) + return line_ind + def indent_to_spaces(self, indent): """ Converts indentation to spaces respecting vim settings. """ indent = self._strip_tabs(indent, self.ts) diff --git a/plugin/UltiSnips/__init__.py b/plugin/UltiSnips/__init__.py index 4b4f021..3fc7ba3 100644 --- a/plugin/UltiSnips/__init__.py +++ b/plugin/UltiSnips/__init__.py @@ -1,8 +1,6 @@ #!/usr/bin/env python # encoding: utf-8 # -from debug import debug # TODO - from functools import wraps import glob import hashlib @@ -275,7 +273,6 @@ class Snippet(object): self._matched = "" self._last_re = None self._globals = globals - self._util = IndentUtil() def __repr__(self): return "Snippet(%s,%s,%s)" % (self._t,self._d,self._opts) @@ -421,11 +418,12 @@ class Snippet(object): return self._matched matched = property(matched) - def launch(self, text_before, parent, start, end = None): + def launch(self, text_before, visual_content, parent, start, end = None): indent = self._INDENT.match(text_before).group(0) lines = (self._v + "\n").splitlines() - self._util.reset() + 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: @@ -433,10 +431,8 @@ class Snippet(object): else: tabs = len(self._TABS.match(line).group(0)) + line_ind = ind_util.ntabs_to_proper_indent(tabs) - line_ind = tabs * self._util.sw * " " - line_ind = self._util.indent_to_spaces(line_ind) - line_ind = self._util.spaces_to_indent(line_ind) if line_num != 0: line_ind = indent + line_ind @@ -444,11 +440,11 @@ class Snippet(object): v = os.linesep.join(v) if parent is None: - return SnippetInstance(StartMarker(start), indent, - v, last_re = self._last_re, globals = self._globals) + return SnippetInstance(StartMarker(start), indent, v, None, None, visual_content = visual_content, + last_re = self._last_re, globals = self._globals) else: - return SnippetInstance(parent, indent, v, start, - end, last_re = self._last_re, globals = self._globals) + return SnippetInstance(parent, indent, v, start, end, visual_content, + last_re = self._last_re, globals = self._globals) class VimState(object): def __init__(self): @@ -656,6 +652,7 @@ class SnippetManager(object): def reset(self, test_error=False): self._test_error = test_error self._snippets = {} + self._visual_content = as_unicode("") while len(self._csnippets): self._current_snippet_is_done() @@ -710,6 +707,29 @@ class SnippetManager(object): if not rv: 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. + """ + sl, sc = map(int, (vim.eval("""line("'<")"""), vim.eval("""virtcol("'<")"""))) + el, ec = map(int, (vim.eval("""line("'>")"""), vim.eval("""virtcol("'>")"""))) + + def _vim_line_with_eol(ln): + return as_unicode(vim.current.buffer[ln] + '\n') + + if sl == el: + text = _vim_line_with_eol(sl-1)[sc-1:ec] + else: + text = _vim_line_with_eol(sl-1)[sc-1:] + for cl in range(sl,el-1): + text += _vim_line_with_eol(cl) + text += _vim_line_with_eol(el-1)[:ec] + + self._visual_content = text + def snippet_dict(self, ft): if ft not in self._snippets: self._snippets[ft] = _SnippetDictionary() @@ -1034,7 +1054,8 @@ class SnippetManager(object): end = Position(pos.line - p_start.line, pos.col) start = Position(end.line, end.col - len(snippet.matched)) - si = snippet.launch(text_before, self._ctab, start, end) + si = snippet.launch(text_before, self._visual_content, self._ctab, start, end) + self._visual_content = "" self._update_vim_buffer() @@ -1045,7 +1066,8 @@ class SnippetManager(object): self._vb = VimBuffer(text_before, after) start = Position(lineno-1, len(text_before)) - self._csnippets.append(snippet.launch(text_before, None, start)) + self._csnippets.append(snippet.launch(text_before, self._visual_content, None, start)) + self._visual_content = "" self._vb.replace_lines(lineno-1, lineno-1, self._cs._current_text) diff --git a/test.py b/test.py index 73c93f9..1d31d49 100755 --- a/test.py +++ b/test.py @@ -2577,9 +2577,79 @@ ${1:Welt} }}}""") Ball }}}""" +################### +# ${VISUAL} tests # +################### +class Visual_NoVisualSelection_Ignore(_VimTest): + snippets = ("test", "h${VISUAL}b") + keys = "test" + EX + "abc" + wanted = "hbabc" +class Visual_SelectOneWord(_VimTest): + snippets = ("test", "h${VISUAL}b") + keys = "blablub" + ESC + "0v6l" + EX + "test" + EX + wanted = "hblablubb" +class Visual_ExpandTwice(_VimTest): + snippets = ("test", "h${VISUAL}b") + keys = "blablub" + ESC + "0v6l" + EX + "test" + EX + "\ntest" + EX + wanted = "hblablubb\nhb" +class Visual_SelectOneWord_TwiceVisual(_VimTest): + snippets = ("test", "h${VISUAL}b${VISUAL}a") + keys = "blablub" + ESC + "0v6l" + EX + "test" + EX + wanted = "hblablubbblabluba" +class Visual_SelectOneWord_Inword(_VimTest): + snippets = ("test", "h${VISUAL}b", "Description", "i") + keys = "blablub" + ESC + "0lv4l" + EX + "test" + EX + wanted = "bhlablubb" +class Visual_SelectOneWord_TillEndOfLine(_VimTest): + snippets = ("test", "h${VISUAL}b", "Description", "i") + keys = "blablub" + ESC + "0v$" + EX + "test" + EX + ESC + "o" + wanted = "hblablub\nb" +class Visual_SelectOneWordWithTabstop_TillEndOfLine(_VimTest): + snippets = ("test", "h${2:ahh}${VISUAL}${1:ups}b", "Description", "i") + keys = "blablub" + ESC + "0v$" + EX + "test" + EX + "mmm" + JF + "n" + JF + "done" + ESC + "o" + wanted = "hnblablub\nmmmbdone" +class Visual_InDefaultText_SelectOneWord_NoOverwrite(_VimTest): + snippets = ("test", "h${1:${VISUAL}}b") + keys = "blablub" + ESC + "0v6l" + EX + "test" + EX + JF + "hello" + wanted = "hblablubbhello" +class Visual_InDefaultText_SelectOneWord(_VimTest): + snippets = ("test", "h${1:${VISUAL}}b") + keys = "blablub" + ESC + "0v6l" + EX + "test" + EX + "hello" + wanted = "hhellob" +class Visual_CrossOneLine(_VimTest): + snippets = ("test", "h${VISUAL}b") + keys = "bla blub\n helloi" + ESC + "0k4lvjll" + EX + "test" + EX + wanted = "bla hblub\n hellobi" + +class Visual_LineSelect(_VimTest): + snippets = ("test", "h${VISUAL}b") + keys = "hello\nnice\nworld" + ESC + "Vkk" + EX + "test" + EX + wanted = "hhello\nnice\nworld\nb" +class Visual_InDefaultText_LineSelect_NoOverwrite(_VimTest): + snippets = ("test", "h${1:bef${VISUAL}aft}b") + keys = "hello\nnice\nworld" + ESC + "Vkk" + EX + "test" + EX + JF + "hi" + wanted = "hbefhello\nnice\nworld\naftbhi" +class Visual_InDefaultText_LineSelect_Overwrite(_VimTest): + snippets = ("test", "h${1:bef${VISUAL}aft}b") + keys = "hello\nnice\nworld" + ESC + "Vkk" + EX + "test" + EX + "jup" + JF + "hi" + wanted = "hjupbhi" +class Visual_LineSelect_CheckIndent(_VimTest): + snippets = ("test", "beg\n\t${VISUAL}\nend") + keys = "hello\nnice\nworld" + ESC + "Vkk" + EX + "test" + EX + wanted = "beg\n\thello\n\tnice\n\tworld\nend" + +class Visual_LineSelect_CheckIndentTwice(_VimTest): + snippets = ("test", "beg\n\t${VISUAL}\nend") + keys = " hello\n nice\n\tworld" + ESC + "Vkk" + EX + "test" + EX + wanted = "beg\n\t hello\n\t nice\n\t\tworld\nend" + +class Visual_LineSelect_WithTabStop(_VimTest): + snippets = ("test", "beg\n\t${VISUAL}\n\t${1:here_we_go}\nend") + keys = "hello\nnice\nworld" + ESC + "Vkk" + EX + "test" + EX + "super" + JF + "done" + wanted = "beg\n\thello\n\tnice\n\tworld\n\tsuper\nenddone" ########################################################################### # END OF TEST #