From 3a3e56a987091ed9622eca810d8185875a44b12b Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Tue, 29 Mar 2016 16:48:41 +0600 Subject: [PATCH] grant access to visual to context and actions Grants access to: * context match condition for context snippets (via snip.visual_text and snip.visual_mode); * pre/post actions (via same variable); * context match condition to (!) lastly selected placeholder, so it is possible now to use autotrigger snippets, that are activated by simply typing letter while tabstop is selected; * python interpolations to lastly selected placeholder; --- doc/UltiSnips.txt | 31 ++++++++++++++++ pythonx/UltiSnips/snippet/definition/_base.py | 25 +++++++++---- pythonx/UltiSnips/snippet/source/_base.py | 6 ++-- .../snippet/source/_snippet_dictionary.py | 6 ++-- pythonx/UltiSnips/snippet_manager.py | 33 +++++++++++++---- .../UltiSnips/text_objects/_python_code.py | 35 +++++++++++++++++-- .../text_objects/_snippet_instance.py | 1 + pythonx/UltiSnips/vim_state.py | 19 +++++++++- test/test_Autotrigger.py | 13 +++++++ test/test_ContextSnippets.py | 15 ++++++++ test/test_Interpolation.py | 20 +++++++++++ test/test_UltiSnipFunc.py | 3 +- 12 files changed, 185 insertions(+), 22 deletions(-) diff --git a/doc/UltiSnips.txt b/doc/UltiSnips.txt index b9b9ad9..45c1fd5 100644 --- a/doc/UltiSnips.txt +++ b/doc/UltiSnips.txt @@ -939,6 +939,14 @@ The 'snip' object provides some properties as well: > snip.ft: The current filetype. + snip.p: + Last selected placeholder. Will contain placeholder object with + following properties: + + 'current_text' - text in the placeholder on the moment of selection; + 'start' - placeholder start on the moment of selection; + 'end' - placeholder end on the moment of selection; + For your convenience, the 'snip' object also provides the following operators: > @@ -1397,6 +1405,15 @@ Global variable `snip` will be available with following properties: - 'to_vim_cursor()' - returns 1-indexed cursor, suitable for assigning to 'vim.current.window.cursor'; 'snip.line' and 'snip.column' - aliases for cursor position (zero-indexed); + 'snip.visual_mode' - ('v', 'V', '^V', see |visual-mode|); + 'snip.visual_text' - last visually-selected text; + 'snip.last_placeholder' - last active placeholder from previous snippet + with following properties: + + - 'current_text' - text in the placeholder on the moment of selection; + - 'start' - placeholder start on the moment of selection; + - 'end' - placeholder end on the moment of selection; + ------------------- SNIP ------------------- @@ -1463,6 +1480,20 @@ endsnippet That snippet will expand to 'var1 +=' after line, which begins from 'var1 :='. + *UltiSnips-capture-placeholder* + +You can capture placeholder text from previous snippet by using following +trick: +------------------- SNIP ------------------- +snippet = "desc" "snip.last_placeholder" Ae +`!p snip.rv = snip.context.current_text` == nil +endsnippet +------------------- SNAP ------------------- + +That snippet will be expanded only if you will replace selected tabstop in +other snippet (like, as in 'if ${1:var}') and will replace that tabstop by +tabstop value following by ' == nil'. + 4.10 Snippets actions *UltiSnips-snippet-actions* --------------------- diff --git a/pythonx/UltiSnips/snippet/definition/_base.py b/pythonx/UltiSnips/snippet/definition/_base.py index 77c1f63..6d7b13f 100644 --- a/pythonx/UltiSnips/snippet/definition/_base.py +++ b/pythonx/UltiSnips/snippet/definition/_base.py @@ -87,14 +87,25 @@ class SnippetDefinition(object): return match return False - def _context_match(self): + def _context_match(self, visual_content): # skip on empty buffer if len(vim.current.buffer) == 1 and vim.current.buffer[0] == "": return - return self._eval_code('snip.context = ' + self._context_code, { - 'context': None - }).context + 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([ @@ -110,7 +121,7 @@ class SnippetDefinition(object): 'buffer': current.buffer, 'line': current.window.cursor[0]-1, 'column': current.window.cursor[1]-1, - 'cursor': SnippetUtilCursor(current.window.cursor) + 'cursor': SnippetUtilCursor(current.window.cursor), } locals.update(additional_locals) @@ -225,7 +236,7 @@ class SnippetDefinition(object): """The matched context.""" return self._context - def matches(self, before): + 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 @@ -267,7 +278,7 @@ class SnippetDefinition(object): self._context = None if match and self._context_code: - self._context = self._context_match() + self._context = self._context_match(visual_content) if not self.context: match = False diff --git a/pythonx/UltiSnips/snippet/source/_base.py b/pythonx/UltiSnips/snippet/source/_base.py index aa38af3..93ba5b0 100644 --- a/pythonx/UltiSnips/snippet/source/_base.py +++ b/pythonx/UltiSnips/snippet/source/_base.py @@ -31,7 +31,8 @@ class SnippetSource(object): deep_extends = self.get_deep_extends(base_filetypes) return [ft for ft in deep_extends if ft in self._snippets] - def get_snippets(self, filetypes, before, possible, autotrigger_only): + def get_snippets(self, filetypes, before, possible, autotrigger_only, + visual_content): """Returns the snippets for all 'filetypes' (in order) and their parents matching the text 'before'. If 'possible' is true, a partial match is enough. Base classes can override this method to provide means @@ -44,7 +45,8 @@ class SnippetSource(object): for ft in self._get_existing_deep_extends(filetypes): snips = self._snippets[ft] result.extend(snips.get_matching_snippets(before, possible, - autotrigger_only)) + autotrigger_only, + visual_content)) return result def get_clear_priority(self, filetypes): diff --git a/pythonx/UltiSnips/snippet/source/_snippet_dictionary.py b/pythonx/UltiSnips/snippet/source/_snippet_dictionary.py index b5b9a5b..d695902 100644 --- a/pythonx/UltiSnips/snippet/source/_snippet_dictionary.py +++ b/pythonx/UltiSnips/snippet/source/_snippet_dictionary.py @@ -16,7 +16,8 @@ class SnippetDictionary(object): """Add 'snippet' to this dictionary.""" self._snippets.append(snippet) - def get_matching_snippets(self, trigger, potentially, autotrigger_only): + def get_matching_snippets(self, trigger, potentially, autotrigger_only, + visual_content): """Returns all snippets matching the given trigger. If 'potentially' is true, returns all that could_match(). @@ -34,7 +35,8 @@ class SnippetDictionary(object): all_snippets = [s for s in all_snippets if s.has_option('A')] if not potentially: - return [s for s in all_snippets if s.matches(trigger)] + return [s for s in all_snippets if s.matches(trigger, + visual_content)] else: return [s for s in all_snippets if s.could_match(trigger)] diff --git a/pythonx/UltiSnips/snippet_manager.py b/pythonx/UltiSnips/snippet_manager.py index ffb26fe..9cc82e9 100644 --- a/pythonx/UltiSnips/snippet_manager.py +++ b/pythonx/UltiSnips/snippet_manager.py @@ -137,6 +137,7 @@ class SnippetManager(object): SnipMateFileSource()) self._should_update_textobjects = False + self._should_reset_visual = False self._reinit() @@ -269,7 +270,7 @@ class SnippetManager(object): snip = UltiSnipsSnippetDefinition(0, trigger, value, description, options, {}, '', context, actions) - if not trigger or snip.matches(before): + if not trigger or snip.matches(before, self._visual_content): self._do_snippet(snip, before) return True else: @@ -489,6 +490,7 @@ class SnippetManager(object): def _jump(self, backwards=False): """Helper method that does the actual jump.""" if self._should_update_textobjects: + self._should_reset_visual = False self._cursor_moved() # we need to set 'onemore' there, because of limitations of the vim @@ -526,18 +528,31 @@ class SnippetManager(object): and ntab.start - self._ctab.end == Position(0, 1) and ntab.end - ntab.start == Position(0, 1)): ntab_short_and_near = True - if ntab.number == 0: - self._current_snippet_is_done() + self._ctab = ntab + + # Run interpolations again to update new placeholder + # values, binded to currently newly jumped placeholder. + self._visual_content.conserve_placeholder(self._ctab) + self._cs.current_placeholder = \ + self._visual_content.placeholder + self._should_reset_visual = False + self._csnippets[0].update_textobjects() + self._vstate.remember_buffer(self._csnippets[0]) + + if ntab.number == 0 and self._csnippets: + self._current_snippet_is_done() else: # This really shouldn't happen, because a snippet should # have been popped when its final tabstop was used. # Cleanup by removing current snippet and recursing. self._current_snippet_is_done() jumped = self._jump(backwards) + if jumped: - self._vstate.remember_position() - self._vstate.remember_unnamed_register(self._ctab.current_text) + if self._ctab: + self._vstate.remember_position() + self._vstate.remember_unnamed_register(self._ctab.current_text) if not ntab_short_and_near: self._ignore_movements = True @@ -619,7 +634,8 @@ class SnippetManager(object): filetypes, before, partial, - autotrigger_only + autotrigger_only, + self._visual_content ) for snippet in possible_snippets: @@ -835,6 +851,11 @@ class SnippetManager(object): finally: self._last_inserted_char = inserted_char + if self._should_reset_visual and self._visual_content.mode == '': + self._visual_content.reset() + + self._should_reset_visual = True + UltiSnips_Manager = SnippetManager( # pylint:disable=invalid-name vim.eval('g:UltiSnipsExpandTrigger'), diff --git a/pythonx/UltiSnips/text_objects/_python_code.py b/pythonx/UltiSnips/text_objects/_python_code.py index e2a1459..5404dca 100644 --- a/pythonx/UltiSnips/text_objects/_python_code.py +++ b/pythonx/UltiSnips/text_objects/_python_code.py @@ -10,6 +10,7 @@ from UltiSnips import _vim from UltiSnips.compatibility import as_unicode from UltiSnips.indent_util import IndentUtil from UltiSnips.text_objects._base import NoneditableTextObject +from UltiSnips.vim_state import _Placeholder import UltiSnips.snippet_manager @@ -95,12 +96,15 @@ class SnippetUtil(object): """ - def __init__(self, initial_indent, vmode, vtext, context): + def __init__(self, initial_indent, vmode, vtext, context, parent): self._ind = IndentUtil() self._visual = _VisualContent(vmode, vtext) self._initial_indent = self._ind.indent_to_spaces(initial_indent) self._reset('') self._context = context + self._start = parent.start + self._end = parent.end + self._parent = parent def _reset(self, cur): """Gets the snippet ready for another update. @@ -207,6 +211,13 @@ class SnippetUtil(object): """Content of visual expansions.""" return self._visual + @property + def p(self): + if self._parent.current_placeholder: + return self._parent.current_placeholder + else: + return _Placeholder('', 0, 0) + @property def context(self): return self._context @@ -234,6 +245,24 @@ class SnippetUtil(object): """Same as shift.""" self.shift(other) + @property + def snippet_start(self): + """ + Returns start of the snippet in format (line, column). + """ + return self._start + + @property + def snippet_end(self): + """ + Returns end of the snippet in format (line, column). + """ + return self._end + + @property + def buffer(self): + return _vim.buf + class PythonCode(NoneditableTextObject): @@ -250,9 +279,9 @@ class PythonCode(NoneditableTextObject): mode = snippet.visual_content.mode context = snippet.context break - except AttributeError: + except AttributeError as e: snippet = snippet._parent # pylint:disable=protected-access - self._snip = SnippetUtil(token.indent, mode, text, context) + self._snip = SnippetUtil(token.indent, mode, text, context, snippet) self._codes = (( 'import re, os, vim, string, random', diff --git a/pythonx/UltiSnips/text_objects/_snippet_instance.py b/pythonx/UltiSnips/text_objects/_snippet_instance.py index e3e3158..60c1db1 100644 --- a/pythonx/UltiSnips/text_objects/_snippet_instance.py +++ b/pythonx/UltiSnips/text_objects/_snippet_instance.py @@ -34,6 +34,7 @@ class SnippetInstance(EditableTextObject): self.locals = {'match': last_re, 'context': context} self.globals = globals self.visual_content = visual_content + self.current_placeholder = None EditableTextObject.__init__(self, parent, start, end, initial_text) diff --git a/pythonx/UltiSnips/vim_state.py b/pythonx/UltiSnips/vim_state.py index 4d15842..18914ee 100644 --- a/pythonx/UltiSnips/vim_state.py +++ b/pythonx/UltiSnips/vim_state.py @@ -3,12 +3,13 @@ """Some classes to conserve Vim's state for comparing over time.""" -from collections import deque +from collections import deque, namedtuple from UltiSnips import _vim from UltiSnips.compatibility import as_unicode, byte2col from UltiSnips.position import Position +_Placeholder = namedtuple('_FrozenPlaceholder', ['current_text', 'start', 'end']) class VimPosition(Position): @@ -113,6 +114,7 @@ class VisualContentPreserver(object): """Forget the preserved state.""" self._mode = '' self._text = as_unicode('') + self._placeholder = None def conserve(self): """Save the last visual selection ond the mode it was made in.""" @@ -135,6 +137,16 @@ class VisualContentPreserver(object): text += _vim_line_with_eol(el - 1)[:ec + 1] self._text = text + def conserve_placeholder(self, placeholder): + if placeholder: + self._placeholder = _Placeholder( + placeholder.current_text, + placeholder.start, + placeholder.end + ) + else: + self._placeholder = None + @property def text(self): """The conserved text.""" @@ -144,3 +156,8 @@ class VisualContentPreserver(object): def mode(self): """The conserved visualmode().""" return self._mode + + @property + def placeholder(self): + """Returns latest selected placeholder.""" + return self._placeholder diff --git a/test/test_Autotrigger.py b/test/test_Autotrigger.py index bbac8b4..1534185 100644 --- a/test/test_Autotrigger.py +++ b/test/test_Autotrigger.py @@ -54,3 +54,16 @@ class Autotrigger_WillProduceNoExceptionWithVimLowerThan214(_VimTest): """} keys = 'abc' wanted = 'abc' + + +class Autotrigger_CanMatchPreviouslySelectedPlaceholder(_VimTest): + files = { 'us/all.snippets': r""" + snippet if "desc" + if ${1:var}: pass + endsnippet + snippet = "desc" "snip.last_placeholder" Ae + `!p snip.rv = snip.context.current_text` == nil + endsnippet + """} + keys = 'if' + EX + '=' + ESC + 'o=' + wanted = 'if var == nil: pass\n=' diff --git a/test/test_ContextSnippets.py b/test/test_ContextSnippets.py index f178b40..14e048a 100644 --- a/test/test_ContextSnippets.py +++ b/test/test_ContextSnippets.py @@ -149,3 +149,18 @@ class ContextSnippets_ContextIsClearedBeforeExpand(_VimTest): keys = "e" + EX + " " + "e" + EX wanted = "1 1" + +class ContextSnippets_ContextHasAccessToVisual(_VimTest): + files = { 'us/all.snippets': r""" + snippet test "desc" "snip.visual_text == '123'" we + Yes + endsnippet + + snippet test "desc" w + No + endsnippet + """} + + keys = "123" + ESC + "vhh" + EX + "test" + EX + " zzz" + ESC + \ + "vhh" + EX + "test" + EX + wanted = "Yes No" diff --git a/test/test_Interpolation.py b/test/test_Interpolation.py index e8e8bae..f51935b 100644 --- a/test/test_Interpolation.py +++ b/test/test_Interpolation.py @@ -458,6 +458,26 @@ class PythonVisual_LineSelect_Simple(_VimTest): keys = 'hello\nnice\nworld' + ESC + 'Vkk' + EX + 'test' + EX wanted = 'hVhello\nnice\nworld\nb' +class PythonVisual_HasAccessToSelectedPlaceholders(_VimTest): + snippets = ( + 'test', + """${1:first} ${2:second} (`!p +snip.rv = "placeholder: " + snip.p.current_text`)""" + ) + keys = 'test' + EX + ESC + "otest" + EX + JF + ESC + wanted = """first second (placeholder: first) +first second (placeholder: second)""" + +class PythonVisual_HasAccessToZeroPlaceholders(_VimTest): + snippets = ( + 'test', + """${1:first} ${2:second} (`!p +snip.rv = "placeholder: " + snip.p.current_text`)""" + ) + keys = 'test' + EX + ESC + "otest" + EX + JF + JF + JF + JF + wanted = """first second (placeholder: first second (placeholder: )) +first second (placeholder: )""" + # Tests for https://bugs.launchpad.net/bugs/1259349 diff --git a/test/test_UltiSnipFunc.py b/test/test_UltiSnipFunc.py index a0dde3d..fda3372 100644 --- a/test/test_UltiSnipFunc.py +++ b/test/test_UltiSnipFunc.py @@ -154,7 +154,8 @@ from UltiSnips.snippet.source import SnippetSource from UltiSnips.snippet.definition import UltiSnipsSnippetDefinition class MySnippetSource(SnippetSource): - def get_snippets(self, filetypes, before, possible, autotrigger_only): + def get_snippets(self, filetypes, before, possible, autotrigger_only, + visual_content): if before.endswith('blumba') and autotrigger_only == False: return [ UltiSnipsSnippetDefinition(