Merge pull request #446 from seletskiy/context-snippets
New feature: Context-aware snippets
This commit is contained in:
commit
d6098193ef
@ -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|
|
||||
@ -653,6 +654,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
|
||||
@ -815,9 +821,10 @@ 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.
|
||||
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: >
|
||||
|
||||
@ -1294,6 +1301,95 @@ 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
|
||||
|
||||
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.
|
||||
|
||||
The following python modules are automatically imported into the scope before
|
||||
'expression' is evaluated: 're', 'os', 'vim', 'string', 'random'.
|
||||
|
||||
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 the current line you need to use 'line-1'.
|
||||
|
||||
------------------- 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 the previous line is starting
|
||||
from 'if err' prefix.
|
||||
|
||||
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
|
||||
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 the default 'if' snippet will be expanded.
|
||||
|
||||
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:
|
||||
|
||||
------------------- 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 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 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 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
|
||||
`!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*
|
||||
|
||||
|
@ -5,6 +5,8 @@
|
||||
|
||||
import re
|
||||
|
||||
import vim
|
||||
|
||||
from UltiSnips import _vim
|
||||
from UltiSnips.compatibility import as_unicode
|
||||
from UltiSnips.indent_util import IndentUtil
|
||||
@ -43,7 +45,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 +55,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 +82,31 @@ 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'),
|
||||
'context["match"] = ' + self._context_code,
|
||||
''
|
||||
])
|
||||
|
||||
context = {'match': False}
|
||||
locals = {
|
||||
'context': context,
|
||||
'window': current.window,
|
||||
'buffer': current.buffer,
|
||||
'line': current.window.cursor[0],
|
||||
'column': current.window.cursor[1],
|
||||
'cursor': current.window.cursor,
|
||||
}
|
||||
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):
|
||||
"""The 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()
|
||||
if not self.context:
|
||||
match = False
|
||||
|
||||
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()
|
||||
|
@ -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)
|
||||
|
@ -69,8 +69,12 @@ class SnippetFileSource(SnippetSource):
|
||||
if ft in self._snippets:
|
||||
del self._snippets[ft]
|
||||
del self._extends[ft]
|
||||
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):
|
||||
|
@ -65,12 +65,19 @@ 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()
|
||||
|
||||
context = None
|
||||
if 'e' in opts:
|
||||
left = remain[:-1].rfind('"')
|
||||
if left != -1 and left != 0:
|
||||
context, remain = remain[left:].strip('"'), remain[:left]
|
||||
|
||||
# Get and strip description if it exists
|
||||
remain = remain.strip()
|
||||
if len(remain.split()) > 1 and remain[-1] == '"':
|
||||
@ -103,9 +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,
|
||||
definition = UltiSnipsSnippetDefinition(
|
||||
priority, trig, content,
|
||||
descr, opts, python_globals,
|
||||
'%s:%i' % (filename, start_line_index)),)
|
||||
'%s:%i' % (filename, start_line_index),
|
||||
context)
|
||||
return 'snippet', (definition,)
|
||||
else:
|
||||
return 'error', ("Invalid snippet type: '%s'" % snip, lines.line_index)
|
||||
|
||||
|
@ -205,18 +205,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)
|
||||
@ -574,6 +575,11 @@ class SnippetManager(object):
|
||||
if not before:
|
||||
return False
|
||||
snippets = self._snips(before, False)
|
||||
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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
127
test/test_ContextSnippets.py
Normal file
127
test/test_ContextSnippets.py
Normal file
@ -0,0 +1,127 @@
|
||||
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
|
||||
}"""
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
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"
|
||||
|
||||
|
||||
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"
|
@ -159,7 +159,8 @@ 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 []
|
||||
""")
|
||||
|
Loading…
x
Reference in New Issue
Block a user