Merge pull request #446 from seletskiy/context-snippets

New feature: Context-aware snippets
This commit is contained in:
Holger Rapp 2015-05-01 14:35:19 +02:00
commit d6098193ef
10 changed files with 307 additions and 21 deletions

View File

@ -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
@ -813,11 +819,12 @@ 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: >
@ -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*

View File

@ -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()

View File

@ -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)

View File

@ -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):

View File

@ -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,
descr, opts, python_globals,
'%s:%i' % (filename, start_line_index)),)
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)

View File

@ -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

View File

@ -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

View File

@ -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

View 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"

View File

@ -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 []
""")