From b3aec07053d123c549fd27a4d02368ad092d820c Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Mon, 16 Feb 2015 21:17:40 +0600 Subject: [PATCH 01/12] proof of concept for context snips --- pythonx/UltiSnips/snippet/definition/_base.py | 45 ++++++++++++++++++- .../snippet/source/file/ultisnips.py | 12 ++++- pythonx/UltiSnips/snippet_manager.py | 5 +++ .../text_objects/_snippet_instance.py | 4 +- syntax/snippets.vim | 2 +- 5 files changed, 62 insertions(+), 6 deletions(-) diff --git a/pythonx/UltiSnips/snippet/definition/_base.py b/pythonx/UltiSnips/snippet/definition/_base.py index 6b73e4e..a19cf61 100644 --- a/pythonx/UltiSnips/snippet/definition/_base.py +++ b/pythonx/UltiSnips/snippet/definition/_base.py @@ -4,6 +4,7 @@ """Snippet representation after parsing.""" import re +import vim from UltiSnips import _vim from UltiSnips.compatibility import as_unicode @@ -43,7 +44,7 @@ class SnippetDefinition(object): _TABS = re.compile(r"^\t*") def __init__(self, priority, trigger, value, description, - options, globals, location): + options, globals, location, context): self._priority = int(priority) self._trigger = as_unicode(trigger) self._value = as_unicode(value) @@ -53,6 +54,8 @@ class SnippetDefinition(object): self._last_re = None self._globals = globals self._location = location + self._context_code = context + self._context = None # Make sure that we actually match our trigger in case we are # immediately expanded. @@ -78,6 +81,32 @@ class SnippetDefinition(object): return match return False + def _context_match(self): + current = vim.current + # skip on empty buffer + if len(current.buffer) == 1 and current.buffer[0] == "": + return + + code = "\n".join([ + 'import re, os, vim, string, random', + '\n'.join(self._globals.get('!p', [])).replace('\r\n', '\n'), + 'ctx["match"] = ' + self._context_code, + '' + ]) + + context = {'match': False} + locals = { + 'ctx': context, + 'w': current.window, + 'b': current.buffer, + 'l': current.window.cursor[0], + 'c': current.window.cursor[1], + } + + exec(code, locals) + + return context["match"] + def has_option(self, opt): """Check if the named option is set.""" return opt in self._opts @@ -109,6 +138,11 @@ class SnippetDefinition(object): """Where this snippet was defined.""" return self._location + @property + def context(self): + """Returns matched context.""" + return self._context + def matches(self, trigger): """Returns True if this snippet matches 'trigger'.""" # If user supplies both "w" and "i", it should perhaps be an @@ -152,6 +186,12 @@ class SnippetDefinition(object): if text_before.strip(' \t') != '': self._matched = '' return False + + if match and self._context_code: + self._context = self._context_match() + print(match, self._trigger, self._context) + match = self._context != None + return match def could_match(self, trigger): @@ -236,7 +276,8 @@ class SnippetDefinition(object): snippet_instance = SnippetInstance( self, parent, initial_text, start, end, visual_content, - last_re=self._last_re, globals=self._globals) + last_re=self._last_re, globals=self._globals, + context=self._context) self.instantiate(snippet_instance, initial_text, indent) snippet_instance.update_textobjects() diff --git a/pythonx/UltiSnips/snippet/source/file/ultisnips.py b/pythonx/UltiSnips/snippet/source/file/ultisnips.py index d503c84..16eba94 100644 --- a/pythonx/UltiSnips/snippet/source/file/ultisnips.py +++ b/pythonx/UltiSnips/snippet/source/file/ultisnips.py @@ -65,12 +65,21 @@ def _handle_snippet_or_global(filename, line, lines, python_globals, priority): # 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] == '"': opts = words[-1] remain = remain[:-len(opts) - 1].rstrip() + if 'x' in opts: + left = remain[:-1].rfind('"') + if left != -1 and left != 0: + context, remain = remain[left:].strip('"'), remain[:left] + else: + context = None + + # Get and strip description if it exists remain = remain.strip() if len(remain.split()) > 1 and remain[-1] == '"': @@ -105,7 +114,8 @@ def _handle_snippet_or_global(filename, line, lines, python_globals, priority): elif snip == 'snippet': return 'snippet', (UltiSnipsSnippetDefinition(priority, trig, content, descr, opts, python_globals, - '%s:%i' % (filename, start_line_index)),) + '%s:%i' % (filename, start_line_index), + context),) else: return 'error', ("Invalid snippet type: '%s'" % snip, lines.line_index) diff --git a/pythonx/UltiSnips/snippet_manager.py b/pythonx/UltiSnips/snippet_manager.py index 06810d1..ff65368 100644 --- a/pythonx/UltiSnips/snippet_manager.py +++ b/pythonx/UltiSnips/snippet_manager.py @@ -475,6 +475,7 @@ class SnippetManager(object): elif feedkey: _vim.command('return %s' % _vim.escape(feedkey)) + @err_to_scratch_buffer def _snips(self, before, partial): """Returns all the snippets for the given text before the cursor. @@ -572,6 +573,10 @@ class SnippetManager(object): if not before: return False snippets = self._snips(before, False) + # prefer snippets with context if any + snippets_with_context = [s for s in snippets if s.context] + if snippets_with_context: + snippets = snippets_with_context if not snippets: # No snippet found return False diff --git a/pythonx/UltiSnips/text_objects/_snippet_instance.py b/pythonx/UltiSnips/text_objects/_snippet_instance.py index d5dfc64..99b86ed 100644 --- a/pythonx/UltiSnips/text_objects/_snippet_instance.py +++ b/pythonx/UltiSnips/text_objects/_snippet_instance.py @@ -21,7 +21,7 @@ class SnippetInstance(EditableTextObject): # pylint:disable=protected-access def __init__(self, snippet, parent, initial_text, - start, end, visual_content, last_re, globals): + start, end, visual_content, last_re, globals, context): if start is None: start = Position(0, 0) if end is None: @@ -29,7 +29,7 @@ class SnippetInstance(EditableTextObject): self.snippet = snippet self._cts = 0 - self.locals = {'match': last_re} + self.locals = {'match': last_re, 'context': context} self.globals = globals self.visual_content = visual_content diff --git a/syntax/snippets.vim b/syntax/snippets.vim index aa2da1b..bebd041 100644 --- a/syntax/snippets.vim +++ b/syntax/snippets.vim @@ -78,7 +78,7 @@ syn match snipSnippetTrigger ,".\{-}"\ze\%(\s\+"\%(\s*\S\)\@=[^"]*\%("\s\+[^"[:s syn match snipSnippetTriggerInvalid ,\S\@=.\{-}\S\ze\%(\s\+"[^"]*\%("\s\+[^"[:space:]]\+\s*\|"\s*\)\=\|\s*\)$, contained nextgroup=snipSnippetDocString skipwhite syn match snipSnippetDocString ,"[^"]*\%("\ze\s*\%(\s[^"[:space:]]\+\s*\)\=\)\=$, contained nextgroup=snipSnippetOptions skipwhite syn match snipSnippetOptions ,\S\+, contained contains=snipSnippetOptionFlag -syn match snipSnippetOptionFlag ,[biwrts], contained +syn match snipSnippetOptionFlag ,[biwrtsmx], contained " Command substitution {{{4 From ed8cf49115f27f6bf754ec8255e125193b9edda5 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Mon, 16 Feb 2015 23:18:03 +0600 Subject: [PATCH 02/12] fix tests --- pythonx/UltiSnips/snippet/definition/_base.py | 2 +- .../UltiSnips/snippet/source/file/ultisnips.py | 1 - pythonx/UltiSnips/snippet_manager.py | 18 ++++++++++-------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/pythonx/UltiSnips/snippet/definition/_base.py b/pythonx/UltiSnips/snippet/definition/_base.py index a19cf61..58fd36f 100644 --- a/pythonx/UltiSnips/snippet/definition/_base.py +++ b/pythonx/UltiSnips/snippet/definition/_base.py @@ -44,7 +44,7 @@ class SnippetDefinition(object): _TABS = re.compile(r"^\t*") def __init__(self, priority, trigger, value, description, - options, globals, location, context): + options, globals, location, context=None): self._priority = int(priority) self._trigger = as_unicode(trigger) self._value = as_unicode(value) diff --git a/pythonx/UltiSnips/snippet/source/file/ultisnips.py b/pythonx/UltiSnips/snippet/source/file/ultisnips.py index 16eba94..99de8b6 100644 --- a/pythonx/UltiSnips/snippet/source/file/ultisnips.py +++ b/pythonx/UltiSnips/snippet/source/file/ultisnips.py @@ -79,7 +79,6 @@ def _handle_snippet_or_global(filename, line, lines, python_globals, priority): else: context = None - # Get and strip description if it exists remain = remain.strip() if len(remain.split()) > 1 and remain[-1] == '"': diff --git a/pythonx/UltiSnips/snippet_manager.py b/pythonx/UltiSnips/snippet_manager.py index ff65368..2300b37 100644 --- a/pythonx/UltiSnips/snippet_manager.py +++ b/pythonx/UltiSnips/snippet_manager.py @@ -203,18 +203,19 @@ class SnippetManager(object): @err_to_scratch_buffer def add_snippet(self, trigger, value, description, - options, ft='all', priority=0): + options, ft='all', priority=0, context=None): """Add a snippet to the list of known snippets of the given 'ft'.""" self._added_snippets_source.add_snippet(ft, UltiSnipsSnippetDefinition(priority, trigger, value, - description, options, {}, 'added')) + description, options, {}, 'added', + context)) @err_to_scratch_buffer - def expand_anon(self, value, trigger='', description='', options=''): + def expand_anon(self, value, trigger='', description='', options='', context=None): """Expand an anonymous snippet right here.""" before = _vim.buf.line_till_cursor snip = UltiSnipsSnippetDefinition(0, trigger, value, description, - options, {}, '') + options, {}, '', context) if not trigger or snip.matches(before): self._do_snippet(snip, before) @@ -573,10 +574,11 @@ class SnippetManager(object): if not before: return False snippets = self._snips(before, False) - # prefer snippets with context if any - snippets_with_context = [s for s in snippets if s.context] - if snippets_with_context: - snippets = snippets_with_context + if snippets: + # prefer snippets with context if any + snippets_with_context = [s for s in snippets if s.context] + if snippets_with_context: + snippets = snippets_with_context if not snippets: # No snippet found return False From 0beefd4a19b6865082633edb87a42feebae9064a Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Tue, 17 Feb 2015 00:23:46 +0600 Subject: [PATCH 03/12] fix context match on False value --- pythonx/UltiSnips/snippet/definition/_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pythonx/UltiSnips/snippet/definition/_base.py b/pythonx/UltiSnips/snippet/definition/_base.py index 58fd36f..c720a94 100644 --- a/pythonx/UltiSnips/snippet/definition/_base.py +++ b/pythonx/UltiSnips/snippet/definition/_base.py @@ -189,8 +189,8 @@ class SnippetDefinition(object): if match and self._context_code: self._context = self._context_match() - print(match, self._trigger, self._context) - match = self._context != None + if not self.context: + match = False return match From 66bc2e8f6ea8958688e506dbfaf92b6f9a693f36 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Thu, 16 Apr 2015 11:31:12 +0600 Subject: [PATCH 04/12] documentation, pull-requests fixes --- doc/UltiSnips.txt | 96 +++++++++++++++++++ pythonx/UltiSnips/snippet/definition/_base.py | 15 +-- .../UltiSnips/snippet/definition/snipmate.py | 3 +- .../snippet/source/file/ultisnips.py | 15 +-- 4 files changed, 114 insertions(+), 15 deletions(-) diff --git a/doc/UltiSnips.txt b/doc/UltiSnips.txt index dbbe8fa..3d54571 100644 --- a/doc/UltiSnips.txt +++ b/doc/UltiSnips.txt @@ -653,6 +653,11 @@ The options currently supported are: > Without this option empty lines in snippets definition will have indentation too. + e Context snippets - With this option expansion of snippet can be + controlled not only by previous characters in line, but by any given python + expression. This option can be specified along with other options, + like 'b'. See |UltiSnips-context-snippets| for more info. + The end line is the 'endsnippet' keyword on a line by itself. > endsnippet @@ -818,6 +823,7 @@ The variables automatically defined in python code are: > t - The values of the placeholders, t[1] is the text of ${1}, and so on snip - UltiSnips.TextObjects.SnippetUtil object instance. Has methods that simplify indentation handling. + context - Result of context condition. See |UltiSnips-context-snippets|. The 'snip' object provides the following methods: > @@ -1293,6 +1299,96 @@ clearsnippets trigger1 trigger2 ------------------- SNAP ------------------- +4.9 Context snippets *UltiSnips-context-snippets* + +Context snippets can be enabled by using 'e' option in snippet definition. + +In that case snippet should be defined using this syntax: > + + snippet tab_trigger "description" "expression" options + +'expression' can be any python expression. If 'expression' evaluates to +'True', then this snippet will be chosen for expansion. 'expression' must be +wrapped with double-quotes. + +Following python modules are automatically imported: 're', 'os', 'vim', +'string', 'random'. + +Also, variables are declared in local scope for use in expression: > + 'window' - alias for 'vim.current.window' + 'buffer' - alias for 'vim.current.window.buffer' + 'line' and 'column' - aliases for cursor position + +Keep in mind, that lines in vim numbered from 1, and lists in python starts +from 0, so to access current line you need to use 'line-1' expression. + +------------------- SNIP ------------------- +snippet r "return" "re.match('^\s+if err ', buffer[line-2])" be +return err +endsnippet +------------------- SNAP ------------------- + +That snippet will expand to 'return err' only if previous line is starting +from 'if err' prefix. + +Note: context snippets prioritized over non-context ones. So, if there are +two snippets with the same trigger, and one of them is context snippet and +it's expression evaluates to 'True', then context snippet will be used for +expansion and first will be ignored. + +------------------- SNIP ------------------- +snippet i "if ..." b +if $1 { + $2 +} +endsnippet + +snippet i "if err != nil" "re.match('^\s+[^=]*err\s*:?=', buffer[line-2])" be +if err != nil { + $1 +} +endsnippet +------------------- SNAP ------------------- + +That snippet will expand into 'if err != nil' if previous line will +match 'err :=' prefix, otherwise default 'if' snippet will be expanded. + +It's good idea to move context conditions to separate library, so it can be +used with other UltiSnips user. In that case, library should be imported +using 'global' keyword, like this: + +------------------- SNIP ------------------- +global !p +import my_utils +endglobal + +snippet , "return ..., nil/err" "my_utils.is_return_argument(buffer, line, column)" ie +, `!p if my_utils.is_in_err_condition(): + snip.rv = "err" +else: + snip.rv = "nil"` +endsnippet +------------------- SNAP ------------------- + +That snippet will expand only if cursor is located in the return statement, +and then it will expand either to 'err' or to 'nil' depending in which 'if' +statement it's located. 'is_return_argument' and 'is_in_err_condition' are +part of custom python library which is called 'my_utils' in this example. + +Context condition can return any value which python can use as +condition in it's 'if' statement, and if it's considired 'True', then snippet +will be expanded. Moreover, result of condition will be accessed in the +'context' variable: + +------------------- SNIP ------------------- +snippet + "var +=" "re.match('\s*(.*?)\s*:?=', buffer[line-2])" ie +`!p snip.rv = context.group(1)` += $1 +endsnippet +------------------- SNAP ------------------- + +That snippet will expand to 'var1 +=' after line, which begins from 'var1 :='. + + ============================================================================== 5. UltiSnips and Other Plugins *UltiSnips-other-plugins* diff --git a/pythonx/UltiSnips/snippet/definition/_base.py b/pythonx/UltiSnips/snippet/definition/_base.py index c720a94..9b9c5de 100644 --- a/pythonx/UltiSnips/snippet/definition/_base.py +++ b/pythonx/UltiSnips/snippet/definition/_base.py @@ -4,6 +4,7 @@ """Snippet representation after parsing.""" import re + import vim from UltiSnips import _vim @@ -44,7 +45,7 @@ class SnippetDefinition(object): _TABS = re.compile(r"^\t*") def __init__(self, priority, trigger, value, description, - options, globals, location, context=None): + options, globals, location, context): self._priority = int(priority) self._trigger = as_unicode(trigger) self._value = as_unicode(value) @@ -90,17 +91,17 @@ class SnippetDefinition(object): code = "\n".join([ 'import re, os, vim, string, random', '\n'.join(self._globals.get('!p', [])).replace('\r\n', '\n'), - 'ctx["match"] = ' + self._context_code, + 'context["match"] = ' + self._context_code, '' ]) context = {'match': False} locals = { - 'ctx': context, - 'w': current.window, - 'b': current.buffer, - 'l': current.window.cursor[0], - 'c': current.window.cursor[1], + 'context': context, + 'window': current.window, + 'buffer': current.buffer, + 'line': current.window.cursor[0], + 'column': current.window.cursor[1], } exec(code, locals) diff --git a/pythonx/UltiSnips/snippet/definition/snipmate.py b/pythonx/UltiSnips/snippet/definition/snipmate.py index 7f28212..a77046b 100644 --- a/pythonx/UltiSnips/snippet/definition/snipmate.py +++ b/pythonx/UltiSnips/snippet/definition/snipmate.py @@ -15,7 +15,8 @@ class SnipMateSnippetDefinition(SnippetDefinition): def __init__(self, trigger, value, description, location): SnippetDefinition.__init__(self, self.SNIPMATE_SNIPPET_PRIORITY, - trigger, value, description, '', {}, location) + trigger, value, description, '', {}, location, + None) def instantiate(self, snippet_instance, initial_text, indent): parse_and_instantiate(snippet_instance, initial_text, indent) diff --git a/pythonx/UltiSnips/snippet/source/file/ultisnips.py b/pythonx/UltiSnips/snippet/source/file/ultisnips.py index 99de8b6..0383997 100644 --- a/pythonx/UltiSnips/snippet/source/file/ultisnips.py +++ b/pythonx/UltiSnips/snippet/source/file/ultisnips.py @@ -72,12 +72,11 @@ def _handle_snippet_or_global(filename, line, lines, python_globals, priority): opts = words[-1] remain = remain[:-len(opts) - 1].rstrip() - if 'x' in opts: + context = None + if 'e' in opts: left = remain[:-1].rfind('"') if left != -1 and left != 0: context, remain = remain[left:].strip('"'), remain[:left] - else: - context = None # Get and strip description if it exists remain = remain.strip() @@ -111,10 +110,12 @@ def _handle_snippet_or_global(filename, line, lines, python_globals, priority): if snip == 'global': python_globals[trig].append(content) elif snip == 'snippet': - return 'snippet', (UltiSnipsSnippetDefinition(priority, trig, content, - descr, opts, python_globals, - '%s:%i' % (filename, start_line_index), - context),) + definition = UltiSnipsSnippetDefinition( + priority, trig, content, + descr, opts, python_globals, + '%s:%i' % (filename, start_line_index), + context) + return 'snippet', (definition,) else: return 'error', ("Invalid snippet type: '%s'" % snip, lines.line_index) From 9861a6e6c3799d203d389c04ca75544fa2f1d73a Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Thu, 16 Apr 2015 11:48:13 +0600 Subject: [PATCH 05/12] typo fix in docs --- doc/UltiSnips.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/UltiSnips.txt b/doc/UltiSnips.txt index 3d54571..a8f529e 100644 --- a/doc/UltiSnips.txt +++ b/doc/UltiSnips.txt @@ -1354,7 +1354,7 @@ That snippet will expand into 'if err != nil' if previous line will match 'err :=' prefix, otherwise default 'if' snippet will be expanded. It's good idea to move context conditions to separate library, so it can be -used with other UltiSnips user. In that case, library should be imported +used by other UltiSnips users. In that case, library should be imported using 'global' keyword, like this: ------------------- SNIP ------------------- From 709f8dc93f15d25b849af69e5d34076e73463940 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Thu, 16 Apr 2015 17:40:53 +0600 Subject: [PATCH 06/12] minor doc fix and new cursor variable --- doc/UltiSnips.txt | 2 ++ pythonx/UltiSnips/snippet/definition/_base.py | 1 + 2 files changed, 3 insertions(+) diff --git a/doc/UltiSnips.txt b/doc/UltiSnips.txt index a8f529e..ea60e84 100644 --- a/doc/UltiSnips.txt +++ b/doc/UltiSnips.txt @@ -36,6 +36,7 @@ UltiSnips *snippet* *snippets* *UltiSnips* 4.7.1 Replacement String |UltiSnips-replacement-string| 4.7.2 Demos |UltiSnips-demos| 4.8 Clearing snippets |UltiSnips-clearing-snippets| + 4.9 Context snippets |UltiSnips-context-snippets| 5. UltiSnips and Other Plugins |UltiSnips-other-plugins| 5.1 Existing Integrations |UltiSnips-integrations| 5.2 Extending UltiSnips |UltiSnips-extending| @@ -1317,6 +1318,7 @@ Following python modules are automatically imported: 're', 'os', 'vim', Also, variables are declared in local scope for use in expression: > 'window' - alias for 'vim.current.window' 'buffer' - alias for 'vim.current.window.buffer' + 'cursor' - alias for 'vim.current.cursor' 'line' and 'column' - aliases for cursor position Keep in mind, that lines in vim numbered from 1, and lists in python starts diff --git a/pythonx/UltiSnips/snippet/definition/_base.py b/pythonx/UltiSnips/snippet/definition/_base.py index 9b9c5de..ea2609e 100644 --- a/pythonx/UltiSnips/snippet/definition/_base.py +++ b/pythonx/UltiSnips/snippet/definition/_base.py @@ -102,6 +102,7 @@ class SnippetDefinition(object): 'buffer': current.buffer, 'line': current.window.cursor[0], 'column': current.window.cursor[1], + 'cursor': current.window.cursor, } exec(code, locals) From 87c3a7ba43c6a80c6c150195a8cd1a8816a76d63 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Sun, 19 Apr 2015 11:40:00 +0600 Subject: [PATCH 07/12] fix UltiSnipFunc test --- test/test_UltiSnipFunc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_UltiSnipFunc.py b/test/test_UltiSnipFunc.py index bd60626..bca2fb2 100644 --- a/test/test_UltiSnipFunc.py +++ b/test/test_UltiSnipFunc.py @@ -159,7 +159,7 @@ class MySnippetSource(SnippetSource): if before.endswith('blumba'): return [ UltiSnipsSnippetDefinition( - -100, "blumba", "this is a dynamic snippet", "", "", {}, "blub") + -100, "blumba", "this is a dynamic snippet", "", "", {}, "blub", None) ] return [] """) From 904fbdecf54f265ce231f90cfea7284f2ff314c7 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Sat, 25 Apr 2015 21:38:22 +0600 Subject: [PATCH 08/12] review fixes --- doc/UltiSnips.txt | 52 +++++------ pythonx/UltiSnips/snippet/definition/_base.py | 4 +- pythonx/UltiSnips/snippet_manager.py | 1 - test/test_ContextSnippets.py | 87 +++++++++++++++++++ 4 files changed, 114 insertions(+), 30 deletions(-) create mode 100644 test/test_ContextSnippets.py diff --git a/doc/UltiSnips.txt b/doc/UltiSnips.txt index ea60e84..8cf3fbb 100644 --- a/doc/UltiSnips.txt +++ b/doc/UltiSnips.txt @@ -655,9 +655,9 @@ The options currently supported are: > indentation too. e Context snippets - With this option expansion of snippet can be - controlled not only by previous characters in line, but by any given python - expression. This option can be specified along with other options, - like 'b'. See |UltiSnips-context-snippets| for more info. + controlled not only by previous characters in line, but by any given + python expression. This option can be specified along with other + options, like 'b'. See |UltiSnips-context-snippets| for more info. The end line is the 'endsnippet' keyword on a line by itself. > @@ -819,11 +819,11 @@ output is ignored. The variables automatically defined in python code are: > - fn - The current filename - path - The complete path to the current file - t - The values of the placeholders, t[1] is the text of ${1}, and so on - snip - UltiSnips.TextObjects.SnippetUtil object instance. Has methods that - simplify indentation handling. + fn - The current filename + path - The complete path to the current file + t - The values of the placeholders, t[1] is the text of ${1}, etc. + snip - UltiSnips.TextObjects.SnippetUtil object instance. Has methods + that simplify indentation handling. context - Result of context condition. See |UltiSnips-context-snippets|. The 'snip' object provides the following methods: > @@ -1308,21 +1308,21 @@ In that case snippet should be defined using this syntax: > snippet tab_trigger "description" "expression" options -'expression' can be any python expression. If 'expression' evaluates to -'True', then this snippet will be chosen for expansion. 'expression' must be -wrapped with double-quotes. +The 'expression' can be any python expression. If 'expression' evaluates to +'True', then this snippet will be chosen for expansion. The 'expression' must +be wrapped with double-quotes. -Following python modules are automatically imported: 're', 'os', 'vim', -'string', 'random'. +The following python modules are automatically imported into the scope before +'expression' is evaluated: 're', 'os', 'vim', 'string', 'random'. -Also, variables are declared in local scope for use in expression: > +Also, the following variables are defined: 'window' - alias for 'vim.current.window' 'buffer' - alias for 'vim.current.window.buffer' 'cursor' - alias for 'vim.current.cursor' 'line' and 'column' - aliases for cursor position Keep in mind, that lines in vim numbered from 1, and lists in python starts -from 0, so to access current line you need to use 'line-1' expression. +from 0, so to access the current line you need to use 'line-1'. ------------------- SNIP ------------------- snippet r "return" "re.match('^\s+if err ', buffer[line-2])" be @@ -1330,7 +1330,7 @@ return err endsnippet ------------------- SNAP ------------------- -That snippet will expand to 'return err' only if previous line is starting +That snippet will expand to 'return err' only if the previous line is starting from 'if err' prefix. Note: context snippets prioritized over non-context ones. So, if there are @@ -1353,10 +1353,10 @@ endsnippet ------------------- SNAP ------------------- That snippet will expand into 'if err != nil' if previous line will -match 'err :=' prefix, otherwise default 'if' snippet will be expanded. +match 'err :=' prefix, otherwise the default 'if' snippet will be expanded. -It's good idea to move context conditions to separate library, so it can be -used by other UltiSnips users. In that case, library should be imported +It's good idea to move context conditions to a separate module, so it can be +used by other UltiSnips users. In that case, module should be imported using 'global' keyword, like this: ------------------- SNIP ------------------- @@ -1372,15 +1372,15 @@ else: endsnippet ------------------- SNAP ------------------- -That snippet will expand only if cursor is located in the return statement, -and then it will expand either to 'err' or to 'nil' depending in which 'if' +That snippet will expand only if the cursor is located in the return statement, +and then it will expand either to 'err' or to 'nil' depending on which 'if' statement it's located. 'is_return_argument' and 'is_in_err_condition' are -part of custom python library which is called 'my_utils' in this example. +part of custom python module which is called 'my_utils' in this example. -Context condition can return any value which python can use as -condition in it's 'if' statement, and if it's considired 'True', then snippet -will be expanded. Moreover, result of condition will be accessed in the -'context' variable: +Context condition can return any value which python can use as condition in +it's 'if' statement, and if it's considered 'True', then snippet will be +expanded. The evaluated value of 'condition' is available in the 'context' +variable inside the snippet: ------------------- SNIP ------------------- snippet + "var +=" "re.match('\s*(.*?)\s*:?=', buffer[line-2])" ie diff --git a/pythonx/UltiSnips/snippet/definition/_base.py b/pythonx/UltiSnips/snippet/definition/_base.py index ea2609e..0c8e096 100644 --- a/pythonx/UltiSnips/snippet/definition/_base.py +++ b/pythonx/UltiSnips/snippet/definition/_base.py @@ -104,9 +104,7 @@ class SnippetDefinition(object): 'column': current.window.cursor[1], 'cursor': current.window.cursor, } - exec(code, locals) - return context["match"] def has_option(self, opt): @@ -142,7 +140,7 @@ class SnippetDefinition(object): @property def context(self): - """Returns matched context.""" + """The matched context.""" return self._context def matches(self, trigger): diff --git a/pythonx/UltiSnips/snippet_manager.py b/pythonx/UltiSnips/snippet_manager.py index 2300b37..4db07d7 100644 --- a/pythonx/UltiSnips/snippet_manager.py +++ b/pythonx/UltiSnips/snippet_manager.py @@ -476,7 +476,6 @@ class SnippetManager(object): elif feedkey: _vim.command('return %s' % _vim.escape(feedkey)) - @err_to_scratch_buffer def _snips(self, before, partial): """Returns all the snippets for the given text before the cursor. diff --git a/test/test_ContextSnippets.py b/test/test_ContextSnippets.py new file mode 100644 index 0000000..b99936f --- /dev/null +++ b/test/test_ContextSnippets.py @@ -0,0 +1,87 @@ +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * + + +class ContextSnippets_SimpleSnippet(_VimTest): + files = { 'us/all.snippets': r""" + snippet a "desc" "True" e + abc + endsnippet + """} + keys = 'a' + EX + wanted = 'abc' + + +class ContextSnippets_ExpandOnTrue(_VimTest): + files = { 'us/all.snippets': r""" + global !p + def check_context(): + return True + endglobal + + snippet a "desc" "check_context()" e + abc + endsnippet + """} + keys = 'a' + EX + wanted = 'abc' + + +class ContextSnippets_DoNotExpandOnFalse(_VimTest): + files = { 'us/all.snippets': r""" + global !p + def check_context(): + return False + endglobal + + snippet a "desc" "check_context()" e + abc + endsnippet + """} + keys = 'a' + EX + wanted = keys + + + +class ContextSnippets_UseContext(_VimTest): + files = { 'us/all.snippets': r""" + global !p + def wrap(ins): + return "< " + ins + " >" + endglobal + + snippet a "desc" "wrap(buffer[line-1])" e + { `!p snip.rv = context` } + endsnippet + """} + keys = 'a' + EX + wanted = '{ < a > }' + + +class ContextSnippets_SnippetPriority(_VimTest): + files = { 'us/all.snippets': r""" + snippet i "desc" "re.search('err :=', buffer[line-2])" e + if err != nil { + ${1:// pass} + } + endsnippet + + snippet i + if ${1:true} { + ${2:// pass} + } + endsnippet + """} + + keys = r""" + err := some_call() + i""" + EX + JF + """ + i""" + EX + wanted = r""" + err := some_call() + if err != nil { + // pass + } + if true { + // pass + }""" From 1b3ecf4a85bf116e086869b2aa17e5cd118cbf0d Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Mon, 27 Apr 2015 15:42:21 +0600 Subject: [PATCH 09/12] priority test, docs & fmt fixes --- doc/UltiSnips.txt | 6 ++---- test/test_ContextSnippets.py | 16 ++++++++++++++++ test/test_UltiSnipFunc.py | 3 ++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/doc/UltiSnips.txt b/doc/UltiSnips.txt index 8cf3fbb..33dcbf8 100644 --- a/doc/UltiSnips.txt +++ b/doc/UltiSnips.txt @@ -1333,10 +1333,8 @@ endsnippet That snippet will expand to 'return err' only if the previous line is starting from 'if err' prefix. -Note: context snippets prioritized over non-context ones. So, if there are -two snippets with the same trigger, and one of them is context snippet and -it's expression evaluates to 'True', then context snippet will be used for -expansion and first will be ignored. +Note: context snippets prioritized over non-context ones. It makes possible to +use non-context snippets as fallback, if no context matched: ------------------- SNIP ------------------- snippet i "if ..." b diff --git a/test/test_ContextSnippets.py b/test/test_ContextSnippets.py index b99936f..ae5a77f 100644 --- a/test/test_ContextSnippets.py +++ b/test/test_ContextSnippets.py @@ -85,3 +85,19 @@ class ContextSnippets_SnippetPriority(_VimTest): if true { // pass }""" + + +class ContextSnippets_PriorityKeyword(_VimTest): + files = { 'us/all.snippets': r""" + snippet i "desc" "True" e + a + endsnippet + + priority 100 + snippet i + b + endsnippet + """} + + keys = "i" + EX + wanted = "b" diff --git a/test/test_UltiSnipFunc.py b/test/test_UltiSnipFunc.py index bca2fb2..b266b9a 100644 --- a/test/test_UltiSnipFunc.py +++ b/test/test_UltiSnipFunc.py @@ -159,7 +159,8 @@ class MySnippetSource(SnippetSource): if before.endswith('blumba'): return [ UltiSnipsSnippetDefinition( - -100, "blumba", "this is a dynamic snippet", "", "", {}, "blub", None) + -100, "blumba", "this is a dynamic snippet", "", "", {}, "blub", + None) ] return [] """) From 2f355c233fe8d027817398809faa92bc9cad3dde Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Wed, 29 Apr 2015 22:25:21 +0600 Subject: [PATCH 10/12] another little fix in documentation --- doc/UltiSnips.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/UltiSnips.txt b/doc/UltiSnips.txt index 33dcbf8..7449b9b 100644 --- a/doc/UltiSnips.txt +++ b/doc/UltiSnips.txt @@ -1353,7 +1353,7 @@ endsnippet That snippet will expand into 'if err != nil' if previous line will match 'err :=' prefix, otherwise the default 'if' snippet will be expanded. -It's good idea to move context conditions to a separate module, so it can be +It's a good idea to move context conditions to a separate module, so it can be used by other UltiSnips users. In that case, module should be imported using 'global' keyword, like this: From 463e68a61140d6754a060e32dff6af855045eee3 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Fri, 1 May 2015 18:02:20 +0600 Subject: [PATCH 11/12] fix for correct error reporting --- pythonx/UltiSnips/snippet/source/file/_base.py | 8 ++++++-- test/test_ContextSnippets.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/pythonx/UltiSnips/snippet/source/file/_base.py b/pythonx/UltiSnips/snippet/source/file/_base.py index 33754ec..6e639a0 100644 --- a/pythonx/UltiSnips/snippet/source/file/_base.py +++ b/pythonx/UltiSnips/snippet/source/file/_base.py @@ -69,8 +69,12 @@ class SnippetFileSource(SnippetSource): if ft in self._snippets: del self._snippets[ft] del self._extends[ft] - for fn in self._files_for_ft[ft]: - self._parse_snippets(ft, fn) + try: + for fn in self._files_for_ft[ft]: + self._parse_snippets(ft, fn) + except: + del self._files_for_ft[ft] + raise # Now load for the parents for parent_ft in self.get_deep_extends([ft]): if parent_ft != ft and self._needs_update(parent_ft): diff --git a/test/test_ContextSnippets.py b/test/test_ContextSnippets.py index ae5a77f..f3819e7 100644 --- a/test/test_ContextSnippets.py +++ b/test/test_ContextSnippets.py @@ -101,3 +101,15 @@ class ContextSnippets_PriorityKeyword(_VimTest): keys = "i" + EX wanted = "b" + + +class ContextSnippets_ReportError(_VimTest): + files = { 'us/all.snippets': r""" + snippet e "desc" "Tru" e + error + endsnippet + """} + + keys = "e" + EX + wanted = "e" + EX + expected_error = r"NameError: name 'Tru' is not defined" From 50bb61cdc4afb737f8ae8868c9a7ae607a91689e Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Fri, 1 May 2015 18:07:59 +0600 Subject: [PATCH 12/12] another test for error reporting --- test/test_ContextSnippets.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/test_ContextSnippets.py b/test/test_ContextSnippets.py index f3819e7..eea4086 100644 --- a/test/test_ContextSnippets.py +++ b/test/test_ContextSnippets.py @@ -113,3 +113,15 @@ class ContextSnippets_ReportError(_VimTest): keys = "e" + EX wanted = "e" + EX expected_error = r"NameError: name 'Tru' is not defined" + + +class ContextSnippets_ReportErrorOnIndexOutOfRange(_VimTest): + files = { 'us/all.snippets': r""" + snippet e "desc" "buffer[123]" e + error + endsnippet + """} + + keys = "e" + EX + wanted = "e" + EX + expected_error = r"IndexError: line number out of range"