Merge support for snippet-actions.

See *UltiSnips-snippet-actions*.
This commit is contained in:
Holger Rapp 2015-07-31 15:32:10 +02:00
commit c12867d1b9
22 changed files with 1389 additions and 117 deletions

View File

@ -0,0 +1,99 @@
let s:SourcedFile=expand("<sfile>")
function! UltiSnips#bootstrap#Bootstrap()
if exists('s:did_UltiSnips_bootstrap')
return
endif
let s:did_UltiSnips_bootstrap=1
if !exists("g:UltiSnipsUsePythonVersion")
let g:_uspy=":py3 "
if !has("python3")
if !has("python")
if !exists("g:UltiSnipsNoPythonWarning")
echohl WarningMsg
echom "UltiSnips requires py >= 2.7 or any py3"
echohl None
endif
unlet g:_uspy
return
endif
let g:_uspy=":py "
endif
let g:UltiSnipsUsePythonVersion = "<tab>"
else
" Use user-provided value, but check if it's available.
" This uses `has()`, because e.g. `exists(":python3")` is always 2.
if g:UltiSnipsUsePythonVersion == 2 && has('python')
let g:_uspy=":python "
elseif g:UltiSnipsUsePythonVersion == 3 && has('python3')
let g:_uspy=":python3 "
endif
if !exists('g:_uspy')
echohl WarningMsg
echom "UltiSnips: the Python version from g:UltiSnipsUsePythonVersion (".g:UltiSnipsUsePythonVersion.") is not available."
echohl None
return
endif
endif
" Expand our path
exec g:_uspy "import vim, os, sys"
exec g:_uspy "sourced_file = vim.eval('s:SourcedFile')"
exec g:_uspy "while not os.path.exists(os.path.join(sourced_file, 'pythonx')):
\ sourced_file = os.path.dirname(sourced_file)"
exec g:_uspy "module_path = os.path.join(sourced_file, 'pythonx')"
exec g:_uspy "vim.command(\"let g:UltiSnipsPythonPath = '%s'\" % module_path)"
exec g:_uspy "if not hasattr(vim, 'VIM_SPECIAL_PATH'): sys.path.append(module_path)"
exec g:_uspy "from UltiSnips.snippet_manager import UltiSnips_Manager"
endfunction
" The trigger used to expand a snippet.
" NOTE: expansion and forward jumping can, but needn't be the same trigger
if !exists("g:UltiSnipsExpandTrigger")
let g:UltiSnipsExpandTrigger = "<tab>"
endif
" The trigger used to display all triggers that could possible
" match in the current position.
if !exists("g:UltiSnipsListSnippets")
let g:UltiSnipsListSnippets = "<c-tab>"
endif
" The trigger used to jump forward to the next placeholder.
" NOTE: expansion and forward jumping can, but needn't be the same trigger
if !exists("g:UltiSnipsJumpForwardTrigger")
let g:UltiSnipsJumpForwardTrigger = "<c-j>"
endif
" The trigger to jump backward inside a snippet
if !exists("g:UltiSnipsJumpBackwardTrigger")
let g:UltiSnipsJumpBackwardTrigger = "<c-k>"
endif
" Should UltiSnips unmap select mode mappings automagically?
if !exists("g:UltiSnipsRemoveSelectModeMappings")
let g:UltiSnipsRemoveSelectModeMappings = 1
end
" If UltiSnips should remove Mappings, which should be ignored
if !exists("g:UltiSnipsMappingsToIgnore")
let g:UltiSnipsMappingsToIgnore = []
endif
" UltiSnipsEdit will use this variable to decide if a new window
" is opened when editing. default is "normal", allowed are also
" "vertical", "horizontal"
if !exists("g:UltiSnipsEditSplit")
let g:UltiSnipsEditSplit = 'normal'
endif
" A list of directory names that are searched for snippets.
if !exists("g:UltiSnipsSnippetDirectories")
let g:UltiSnipsSnippetDirectories = [ "UltiSnips" ]
endif
" Enable or Disable snipmate snippet expansion.
if !exists("g:UltiSnipsEnableSnipMate")
let g:UltiSnipsEnableSnipMate = 1
endif

View File

@ -38,6 +38,10 @@ UltiSnips *snippet* *snippets* *UltiSnips*
4.7.2 Demos |UltiSnips-demos|
4.8 Clearing snippets |UltiSnips-clearing-snippets|
4.9 Context snippets |UltiSnips-context-snippets|
4.10 Snippet actions |UltiSnips-snippet-actions|
4.10.1 Pre-expand actions |UltiSnips-pre-expand-actions|
4.10.2 Post-expand actions |UltiSnips-post-expand-actions|
4.10.3 Post-jump actions |UltiSnips-post-jump-actions|
5. UltiSnips and Other Plugins |UltiSnips-other-plugins|
5.1 Existing Integrations |UltiSnips-integrations|
5.2 Extending UltiSnips |UltiSnips-extending|
@ -1343,17 +1347,21 @@ 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
Global variable `snip` will be available with following properties:
'snip.window' - alias for 'vim.current.window'
'snip.buffer' - alias for 'vim.current.window.buffer'
'snip.cursor' - cursor object, which behaves like
'vim.current.window.cursor', but zero-indexed and with following
additional methods:
- 'preserve()' - special method for executing pre/post/jump actions;
- 'set(line, column)' - sets cursor to specified line and column;
- '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);
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
snippet r "return" "re.match('^\s+if err ', snip.buffer[snip.line-1])" be
return err
endsnippet
------------------- SNAP -------------------
@ -1371,7 +1379,7 @@ if $1 {
}
endsnippet
snippet i "if err != nil" "re.match('^\s+[^=]*err\s*:?=', buffer[line-2])" be
snippet i "if err != nil" "re.match('^\s+[^=]*err\s*:?=', snip.buffer[snip.line-1])" be
if err != nil {
$1
}
@ -1390,7 +1398,7 @@ global !p
import my_utils
endglobal
snippet , "return ..., nil/err" "my_utils.is_return_argument(buffer, line, column)" ie
snippet , "return ..., nil/err" "my_utils.is_return_argument(snip)" ie
, `!p if my_utils.is_in_err_condition():
snip.rv = "err"
else:
@ -1405,18 +1413,192 @@ 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'
expanded. The evaluated value of 'condition' is available in the 'snip.context'
variable inside the snippet:
------------------- SNIP -------------------
snippet + "var +=" "re.match('\s*(.*?)\s*:?=', buffer[line-2])" ie
`!p snip.rv = context.group(1)` += $1
snippet + "var +=" "re.match('\s*(.*?)\s*:?=', snip.buffer[snip.line-1])" ie
`!p snip.rv = snip.context.group(1)` += $1
endsnippet
------------------- SNAP -------------------
That snippet will expand to 'var1 +=' after line, which begins from 'var1 :='.
4.10 Snippets actions *UltiSnips-snippet-actions*
---------------------
Snippet actions is an arbitrary python code which can be executed at specific
points in lifetime of the snippet.
There are three types of actions:
* Pre-expand - invoked just after trigger condition was matched, but before
snippet actually expanded;
* Post-expand - invoked after snippet was expanded and interpolations
was applied for the first time, but before jump on the first placeholder.
* Jump - invoked just after jump to the next/prev placeholder.
Specified code will be evaluated at stages defined above and same global
variables and modules will be available that are stated in
the *UltiSnips-context-snippets* section.
*UltiSnips-buffer-proxy*
Note: special variable called 'snip.buffer' should be used for all buffer
modifications. Not 'vim.current.buffer' and not 'vim.command("...")', because
of in that case UltiSnips will not be able to track changes buffer from
actions.
'snip.buffer' has the same interface as 'vim.current.window.buffer'.
4.10.1 Pre-expand actions *UltiSnips-pre-expand-actions*
Pre-expand actions can be used to match snippet in one location and then
expand it in the different location. Some useful cases are: correcting
indentation for snippet; expanding snippet for function declaration in another
function body with moving expansion point beyond initial function; performing
extract method refactoring via expanding snippet in different place.
Pre-expand action declared as follows: >
pre_expand "python code here"
snippet ...
endsnippet
Buffer can be modified in pre-expand action code through variable called
'snip.buffer', snippet expansion position will be automatically adjusted.
If cursor line (where trigger was matched) need to be modified, then special
variable method 'snip.cursor.set(line, column)' must be called with the
desired cursor position. In that case UltiSnips will not remove any matched
trigger text and it should be done manually in action code.
To addition to the scope variables defined above 'snip.visual_content' will be
also declared and will contain text that was selected before snippet expansion
(similar to $VISUAL placeholder).
Following snippet will be expanded at 4 spaces indentation level no matter
where it was triggered.
------------------- SNIP -------------------
pre_expand "snip.buffer[snip.line] = ' '*4; snip.cursor.set(line, 4)"
snippet d
def $1():
$0
endsnippet
------------------- SNAP -------------------
Following snippet will move the selected code to the end of file and create
new method definition for it:
------------------- SNIP -------------------
pre_expand "del snip.buffer[snip.line]; snip.buffer.append(''); snip.cursor.set(len(snip.buffer)-1, 0)"
snippet x
def $1():
${2:${VISUAL}}
endsnippet
------------------- SNAP -------------------
4.10.2 Post-expand actions *UltiSnips-post-expand-actions*
Post-expand actions can be used to perform some actions based on the expanded
snippet text. Some cases are: code style formatting (e.g. inserting newlines
before and after method declaration), apply actions depending on python
interpolation result.
Post-expand action declared as follows: >
post_expand "python code here"
snippet ...
endsnippet
Buffer can be modified in post-expand action code through variable called
'snip.buffer', snippet expansion position will be automatically adjusted.
Variables 'snip.snippet_start' and 'snip.snippet_end' will be defined at the
action code scope and will point to positions of the start and end of expanded
snippet accordingly in the form '(line, column)'.
Note: 'snip.snippet_start' and 'snip.snippet_end' will automatically adjust to
the correct positions if post-action will insert or delete lines before
expansion.
Following snippet will expand to method definition and automatically insert
additional newline after end of the snippet. It's very useful to create a
function that will insert as many newlines as required in specific context.
------------------- SNIP -------------------
post_expand "snip.buffer[snip.snippet_end[0]+1:snip.snippet_end[0]+1] = ['']"
snippet d "Description" b
def $1():
$2
endsnippet
------------------- SNAP -------------------
4.10.3 Post-jump actions *UltiSnips-post-jump-actions*
Post-jump actions can be used to trigger some code based on user input into
the placeholders. Notable use cases: expand another snippet after jump or
anonymous snippet after last jump (e.g. perform move method refactoring and
then insert new method invokation); insert heading into TOC after last jump.
Jump-expand action declared as follows: >
post_jump "python code here"
snippet ...
endsnippet
Buffer can be modified in post-expand action code through variable called
'snip.buffer', snippet expansion position will be automatically adjusted.
Next variables and methods will be also defined in the action code scope:
* 'snip.tabstop' - number of tabstop jumped onto;
* 'snip.jump_direction' - '1' if jumped forward and '-1' otherwise;
* 'snip.tabstops' - list with tabstop objects, see above;
* 'snip.snippet_start' - (line, column) of start of the expanded snippet;
* 'snip.snippet_end' - (line, column) of end of the expanded snippet;
* 'snip.expand_anon()' - alias for 'UltiSnips_Manager.expand_anon()';
Tabstop object has several useful properties:
* 'start' - (line, column) of the starting position of the tabstop (also
accessible as 'tabstop.line' and 'tabstop.col').
* 'end' - (line, column) of the ending position;
* 'current_text' - text inside the tabstop.
Following snippet will insert section in the Table of Contents in the vim-help
file:
------------------- SNIP -------------------
post_jump "if snip.tabstop == 0: insert_toc_item(snip.tabstops[1], snip.buffer)"
snippet s "section" b
`!p insert_delimiter_0(snip, t)`$1`!p insert_section_title(snip, t)`
`!p insert_delimiter_1(snip, t)`
$0
endsnippet
------------------- SNAP -------------------
'insert_toc_item' will be called after first jump and will add newly entered
section into the TOC for current file.
Note: It is also possible to trigger snippet expansion from the jump action.
In that case method 'snip.cursor.preserve()' should be called, so UltiSnips
will know that cursor is already at the required position.
Following example will insert method call at the end of file after user jump
out of method declaration snippet.
------------------- SNIP -------------------
global !p
def insert_method_call(name):
vim.command('normal G')
snip.expand_anon(name + '($1)\n')
endglobal
post_jump "if snip.tabstop == 0: insert_method_call(snip.tabstops[1].current_text)"
snippet d "method declaration" b
def $1():
$2
endsnippet
------------------- SNAP -------------------
==============================================================================
5. UltiSnips and Other Plugins *UltiSnips-other-plugins*

View File

@ -3,11 +3,4 @@
"""Entry point for all thinks UltiSnips."""
import vim # pylint:disable=import-error
from UltiSnips.snippet_manager import SnippetManager
UltiSnips_Manager = SnippetManager( # pylint:disable=invalid-name
vim.eval('g:UltiSnipsExpandTrigger'),
vim.eval('g:UltiSnipsJumpForwardTrigger'),
vim.eval('g:UltiSnipsJumpBackwardTrigger'))
from UltiSnips.snippet_manager import UltiSnips_Manager

View File

@ -12,6 +12,8 @@ from UltiSnips.compatibility import col2byte, byte2col, \
as_unicode, as_vimencoding
from UltiSnips.position import Position
from contextlib import contextmanager
class VimBuffer(object):
@ -71,6 +73,25 @@ class VimBuffer(object):
vim.current.window.cursor = pos.line + 1, nbyte
buf = VimBuffer() # pylint:disable=invalid-name
@contextmanager
def toggle_opt(name, new_value):
old_value = eval('&' + name)
command('set {}={}'.format(name, new_value))
try:
yield
finally:
command('set {}={}'.format(name, old_value))
@contextmanager
def save_mark(name):
old_pos = get_mark_pos(name)
try:
yield
finally:
if _is_pos_zero(old_pos):
delete_mark(name)
else:
set_mark_from_pos(name, old_pos)
def escape(inp):
"""Creates a vim-friendly string from a group of
@ -108,7 +129,18 @@ def feedkeys(keys, mode='n'):
Mainly for convenience.
"""
command(as_unicode(r'call feedkeys("%s", "%s")') % (keys, mode))
if eval('mode()') == 'n':
if keys == 'a':
cursor_pos = get_cursor_pos()
cursor_pos[2] = int(cursor_pos[2]) + 1
set_cursor_from_pos(cursor_pos)
if keys in 'ai':
keys = 'startinsert'
if keys == 'startinsert':
command('startinsert')
else:
command(as_unicode(r'call feedkeys("%s", "%s")') % (keys, mode))
def new_scratch_buffer(text):
@ -137,13 +169,15 @@ def select(start, end):
col = col2byte(start.line + 1, start.col)
vim.current.window.cursor = start.line + 1, col
mode = eval('mode()')
move_cmd = ''
if eval('mode()') != 'n':
if mode != 'n':
move_cmd += r"\<Esc>"
if start == end:
# Zero Length Tabstops, use 'i' or 'a'.
if col == 0 or eval('mode()') not in 'i' and \
if col == 0 or mode not in 'i' and \
col < len(buf[start.line]):
move_cmd += 'i'
else:
@ -164,6 +198,32 @@ def select(start, end):
start.line + 1, start.col + 1)
feedkeys(move_cmd)
def set_mark_from_pos(name, pos):
return _set_pos("'" + name, pos)
def get_mark_pos(name):
return _get_pos("'" + name)
def set_cursor_from_pos(pos):
return _set_pos('.', pos)
def get_cursor_pos():
return _get_pos('.')
def delete_mark(name):
try:
return command('delma ' + name)
except:
return False
def _set_pos(name, pos):
return eval("setpos(\"{}\", {})".format(name, pos))
def _get_pos(name):
return eval("getpos(\"{}\")".format(name))
def _is_pos_zero(pos):
return ['0'] * 4 == pos or [0] == pos
def _unmap_select_mode_mapping():
"""This function unmaps select mode mappings if so wished by the user.

View File

@ -0,0 +1,224 @@
# coding=utf8
import vim
import UltiSnips._vim
from UltiSnips.compatibility import as_unicode, as_vimencoding
from UltiSnips.position import Position
from UltiSnips._diff import diff
from UltiSnips import _vim
from contextlib import contextmanager
@contextmanager
def use_proxy_buffer(snippets_stack, vstate):
"""
Forward all changes made in the buffer to the current snippet stack while
function call.
"""
buffer_proxy = VimBufferProxy(snippets_stack, vstate)
old_buffer = _vim.buf
try:
_vim.buf = buffer_proxy
yield
finally:
_vim.buf = old_buffer
buffer_proxy.validate_buffer()
@contextmanager
def suspend_proxy_edits():
"""
Prevents changes being applied to the snippet stack while function call.
"""
if not isinstance(_vim.buf, VimBufferProxy):
yield
else:
try:
_vim.buf._disable_edits()
yield
finally:
_vim.buf._enable_edits()
class VimBufferProxy(_vim.VimBuffer):
"""
Proxy object used for tracking changes that made from snippet actions.
Unfortunately, vim by itself lacks of the API for changing text in
trackable maner.
Vim marks offers limited functionality for tracking line additions and
deletions, but nothing offered for tracking changes withing single line.
Instance of this class is passed to all snippet actions and behaves as
internal vim.current.window.buffer.
All changes that are made by user passed to diff algorithm, and resulting
diff applied to internal snippet structures to ensure they are in sync with
actual buffer contents.
"""
def __init__(self, snippets_stack, vstate):
"""
Instantiate new object.
snippets_stack is a slice of currently active snippets.
"""
self._snippets_stack = snippets_stack
self._buffer = vim.current.buffer
self._change_tick = int(vim.eval("b:changedtick"))
self._forward_edits = True
self._vstate = vstate
def is_buffer_changed_outside(self):
"""
Returns true, if buffer was changed without using proxy object, like
with vim.command() or through internal vim.current.window.buffer.
"""
return self._change_tick < int(vim.eval("b:changedtick"))
def validate_buffer(self):
"""
Raises exception if buffer is changes beyound proxy object.
"""
if self.is_buffer_changed_outside():
raise RuntimeError('buffer was modified using vim.command or ' +
'vim.current.buffer; that changes are untrackable and leads to ' +
'errors in snippet expansion; use special variable `snip.buffer` '
'for buffer modifications.\n\n' +
'See :help UltiSnips-buffer-proxy for more info.')
def __setitem__(self, key, value):
"""
Behaves as vim.current.window.buffer.__setitem__ except it tracks
changes and applies them to the current snippet stack.
"""
if isinstance(key, slice):
value = [as_vimencoding(line) for line in value]
changes = list(self._get_diff(key.start, key.stop, value))
self._buffer[key.start:key.stop] = [
line.strip('\n') for line in value
]
else:
value = as_vimencoding(value)
changes = list(self._get_line_diff(key, self._buffer[key], value))
self._buffer[key] = value
self._change_tick += 1
if self._forward_edits:
for change in changes:
self._apply_change(change)
if self._snippets_stack:
self._vstate.remember_buffer(self._snippets_stack[0])
def __setslice__(self, i, j, text):
"""
Same as __setitem__.
"""
self.__setitem__(slice(i, j), text)
def __getitem__(self, key):
"""
Just passing call to the vim.current.window.buffer.__getitem__.
"""
if isinstance(key, slice):
return [as_unicode(l) for l in self._buffer[key.start:key.stop]]
else:
return as_unicode(self._buffer[key])
def __getslice__(self, i, j):
"""
Same as __getitem__.
"""
return self.__getitem__(slice(i, j))
def __len__(self):
"""
Same as len(vim.current.window.buffer).
"""
return len(self._buffer)
def append(self, line, line_number=-1):
"""
Same as vim.current.window.buffer.append(), but with tracking changes.
"""
if line_number < 0:
line_number = len(self)
if not isinstance(line, list):
line = [line]
self[line_number:line_number] = [as_vimencoding(l) for l in line]
def __delitem__(self, key):
if isinstance(key, slice):
self.__setitem__(key, [])
else:
self.__setitem__(slice(key, key+1), [])
def _get_diff(self, start, end, new_value):
"""
Very fast diffing algorithm when changes are across many lines.
"""
for line_number in range(start, end):
if line_number < 0:
line_number = len(self._buffer) + line_number
yield ('D', line_number, 0, self._buffer[line_number])
if start < 0:
start = len(self._buffer) + start
for line_number in range(0, len(new_value)):
yield ('I', start+line_number, 0, new_value[line_number])
def _get_line_diff(self, line_number, before, after):
"""
Use precise diffing for tracking changes in single line.
"""
if before == '':
for change in self._get_diff(line_number, line_number+1, [after]):
yield change
else:
for change in diff(before, after):
yield (change[0], line_number, change[2], change[3])
def _apply_change(self, change):
"""
Apply changeset to current snippets stack, correctly moving around
snippet itself or its child.
"""
if not self._snippets_stack:
return
line_number = change[1]
column_number = change[2]
line_before = line_number <= self._snippets_stack[0]._start.line
column_before = column_number <= self._snippets_stack[0]._start.col
if line_before and column_before:
direction = 1
if change[0] == 'D':
direction = -1
self._snippets_stack[0]._move(
Position(line_number, 0),
Position(direction, 0)
)
else:
if line_number > self._snippets_stack[0]._end.line:
return
if column_number > self._snippets_stack[0]._end.col:
return
self._snippets_stack[0]._do_edit(change)
def _disable_edits(self):
"""
Temporary disable applying changes to snippets stack. Should be done
while expanding anonymous snippet in the middle of jump to prevent
double tracking.
"""
self._forward_edits = False
def _enable_edits(self):
"""
Enables changes forwarding back.
"""
self._forward_edits = True

View File

@ -65,3 +65,13 @@ class Position(object):
def __repr__(self):
return '(%i,%i)' % (self.line, self.col)
def __getitem__(self, index):
if index > 1:
raise IndexError(
'position can be indexed only 0 (line) and 1 (column)'
)
if index == 0:
return self.line
else:
return self.col

View File

@ -10,8 +10,10 @@ import vim
from UltiSnips import _vim
from UltiSnips.compatibility import as_unicode
from UltiSnips.indent_util import IndentUtil
from UltiSnips.position import Position
from UltiSnips.text import escape
from UltiSnips.text_objects import SnippetInstance
from UltiSnips.text_objects._python_code import SnippetUtilCursor, SnippetUtilForAction
__WHITESPACE_SPLIT = re.compile(r"\s")
def split_at_whitespace(string):
@ -46,7 +48,7 @@ class SnippetDefinition(object):
_TABS = re.compile(r"^\t*")
def __init__(self, priority, trigger, value, description,
options, globals, location, context):
options, globals, location, context, actions):
self._priority = int(priority)
self._trigger = as_unicode(trigger)
self._value = as_unicode(value)
@ -58,6 +60,7 @@ class SnippetDefinition(object):
self._location = location
self._context_code = context
self._context = None
self._actions = actions
# Make sure that we actually match our trigger in case we are
# immediately expanded.
@ -84,29 +87,82 @@ class SnippetDefinition(object):
return False
def _context_match(self):
current = vim.current
# skip on empty buffer
if len(current.buffer) == 1 and current.buffer[0] == "":
if len(vim.current.buffer) == 1 and vim.current.buffer[0] == "":
return
return self._eval_code('snip.context = ' + self._context_code, {
'context': None
}).context
def _eval_code(self, code, additional_locals={}):
code = "\n".join([
'import re, os, vim, string, random',
'\n'.join(self._globals.get('!p', [])).replace('\r\n', '\n'),
'context["match"] = ' + self._context_code,
''
code
])
context = {'match': False}
current = vim.current
locals = {
'context': context,
'window': current.window,
'buffer': current.buffer,
'line': current.window.cursor[0],
'column': current.window.cursor[1],
'cursor': current.window.cursor,
'line': current.window.cursor[0]-1,
'column': current.window.cursor[1]-1,
'cursor': SnippetUtilCursor(current.window.cursor)
}
exec(code, locals)
return context["match"]
locals.update(additional_locals)
snip = SnippetUtilForAction(locals)
exec(code, {'snip': snip})
return snip
def _execute_action(
self,
action,
context,
additional_locals={}
):
mark_to_use = '`'
with _vim.save_mark(mark_to_use):
_vim.set_mark_from_pos(mark_to_use, _vim.get_cursor_pos())
cursor_line_before = _vim.buf.line_till_cursor
locals = {
'context': context,
}
locals.update(additional_locals)
snip = self._eval_code(action, locals)
if snip.cursor.is_set():
vim.current.window.cursor = snip.cursor.to_vim_cursor()
else:
new_mark_pos = _vim.get_mark_pos(mark_to_use)
cursor_invalid = False
if _vim._is_pos_zero(new_mark_pos):
cursor_invalid = True
else:
_vim.set_cursor_from_pos(new_mark_pos)
if cursor_line_before != _vim.buf.line_till_cursor:
cursor_invalid = True
if cursor_invalid:
raise RuntimeError(
'line under the cursor was modified, but ' +
'"snip.cursor" variable is not set; either set set ' +
'"snip.cursor" to new cursor position, or do not ' +
'modify cursor line'
)
return snip
def has_option(self, opt):
"""Check if the named option is set."""
@ -184,6 +240,7 @@ class SnippetDefinition(object):
self._matched = ''
return False
self._context = None
if match and self._context_code:
self._context = self._context_match()
if not self.context:
@ -243,6 +300,65 @@ class SnippetDefinition(object):
objects alive inside of Vim."""
raise NotImplementedError()
def do_pre_expand(self, visual_content, snippets_stack):
if 'pre_expand' in self._actions:
locals = {'buffer': _vim.buf, 'visual_content': visual_content}
snip = self._execute_action(
self._actions['pre_expand'], self._context, locals
)
self._context = snip.context
return snip.cursor.is_set()
else:
return False
def do_post_expand(self, start, end, snippets_stack):
if 'post_expand' in self._actions:
locals = {
'snippet_start': start,
'snippet_end': end,
'buffer': _vim.buf
}
snip = self._execute_action(
self._actions['post_expand'], snippets_stack[-1].context, locals
)
snippets_stack[-1].context = snip.context
return snip.cursor.is_set()
else:
return False
def do_post_jump(
self, tabstop_number, jump_direction, snippets_stack, current_snippet
):
if 'post_jump' in self._actions:
start = current_snippet.start
end = current_snippet.end
locals = {
'tabstop': tabstop_number,
'jump_direction': jump_direction,
'tabstops': current_snippet.get_tabstops(),
'snippet_start': start,
'snippet_end': end,
'buffer': _vim.buf
}
snip = self._execute_action(
self._actions['post_jump'], current_snippet.context, locals
)
current_snippet.context = snip.context
return snip.cursor.is_set()
else:
return False
def launch(self, text_before, visual_content, parent, start, end):
"""Launch this snippet, overwriting the text 'start' to 'end' and
keeping the 'text_before' on the launch line.

View File

@ -16,7 +16,7 @@ class SnipMateSnippetDefinition(SnippetDefinition):
def __init__(self, trigger, value, description, location):
SnippetDefinition.__init__(self, self.SNIPMATE_SNIPPET_PRIORITY,
trigger, value, description, '', {}, location,
None)
None, {})
def instantiate(self, snippet_instance, initial_text, indent):
parse_and_instantiate(snippet_instance, initial_text, indent)

View File

@ -10,3 +10,12 @@ def handle_extends(tail, line_index):
return 'extends', ([p.strip() for p in tail.split(',')],)
else:
return 'error', ("'extends' without file types", line_index)
def handle_action(head, tail, line_index):
if tail:
action = tail.strip('"').replace(r'\"', '"').replace(r'\\\\', r'\\')
return head, (action,)
else:
return 'error', ("'{}' without specified action".format(head),
line_index)

View File

@ -10,7 +10,8 @@ import os
from UltiSnips import _vim
from UltiSnips.snippet.definition import UltiSnipsSnippetDefinition
from UltiSnips.snippet.source.file._base import SnippetFileSource
from UltiSnips.snippet.source.file._common import handle_extends
from UltiSnips.snippet.source.file._common import handle_extends, \
handle_action
from UltiSnips.text import LineIterator, head_tail
@ -53,7 +54,9 @@ def find_all_snippet_files(ft):
return ret
def _handle_snippet_or_global(filename, line, lines, python_globals, priority):
def _handle_snippet_or_global(
filename, line, lines, python_globals, priority, pre_expand
):
"""Parses the snippet that begins at the current line."""
start_line_index = lines.line_index
descr = ''
@ -114,7 +117,7 @@ def _handle_snippet_or_global(filename, line, lines, python_globals, priority):
priority, trig, content,
descr, opts, python_globals,
'%s:%i' % (filename, start_line_index),
context)
context, pre_expand)
return 'snippet', (definition,)
else:
return 'error', ("Invalid snippet type: '%s'" % snip, lines.line_index)
@ -130,14 +133,21 @@ def _parse_snippets_file(data, filename):
python_globals = defaultdict(list)
lines = LineIterator(data)
current_priority = 0
actions = {}
for line in lines:
if not line.strip():
continue
head, tail = head_tail(line)
if head in ('snippet', 'global'):
snippet = _handle_snippet_or_global(filename, line, lines,
python_globals, current_priority)
snippet = _handle_snippet_or_global(
filename, line, lines,
python_globals,
current_priority,
actions
)
actions = {}
if snippet is not None:
yield snippet
elif head == 'extends':
@ -149,6 +159,12 @@ def _parse_snippets_file(data, filename):
current_priority = int(tail.split()[0])
except (ValueError, IndexError):
yield 'error', ('Invalid priority %r' % tail, lines.line_index)
elif head in ['pre_expand', 'post_expand', 'post_jump']:
head, tail = handle_action(head, tail, lines.line_index)
if head == 'error':
yield (head, tail)
else:
actions[head], = tail
elif head and not head.startswith('#'):
yield 'error', ('Invalid line %r' % line.rstrip(), lines.line_index)

View File

@ -8,6 +8,8 @@ from functools import wraps
import os
import platform
import traceback
import vim
from contextlib import contextmanager
from UltiSnips import _vim
from UltiSnips._diff import diff, guess_edit
@ -18,6 +20,7 @@ from UltiSnips.snippet.source import UltiSnipsFileSource, SnipMateFileSource, \
find_all_snippet_files, find_snippet_files, AddedSnippetsSource
from UltiSnips.text import escape
from UltiSnips.vim_state import VimState, VisualContentPreserver
from UltiSnips.buffer_proxy import use_proxy_buffer, suspend_proxy_edits
def _ask_user(a, formatted):
@ -94,6 +97,9 @@ class SnippetManager(object):
self._snippet_sources = []
self._snip_expanded_in_action = False
self._inside_action = False
self._added_snippets_source = AddedSnippetsSource()
self.register_snippet_source('ultisnips_files', UltiSnipsFileSource())
self.register_snippet_source('added', self._added_snippets_source)
@ -205,19 +211,22 @@ class SnippetManager(object):
@err_to_scratch_buffer
def add_snippet(self, trigger, value, description,
options, ft='all', priority=0, context=None):
options, ft='all', priority=0, context=None, actions={}):
"""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',
context))
UltiSnipsSnippetDefinition(priority, trigger, value,
description, options, {}, 'added',
context, actions))
@err_to_scratch_buffer
def expand_anon(self, value, trigger='', description='', options='', context=None):
def expand_anon(
self, value, trigger='', description='', options='',
context=None, actions={}
):
"""Expand an anonymous snippet right here."""
before = _vim.buf.line_till_cursor
snip = UltiSnipsSnippetDefinition(0, trigger, value, description,
options, {}, '', context)
options, {}, '', context, actions)
if not trigger or snip.matches(before):
self._do_snippet(snip, before)
@ -275,6 +284,7 @@ class SnippetManager(object):
if _vim.eval('mode()') not in 'in':
return
if self._ignore_movements:
self._ignore_movements = False
return
@ -430,38 +440,66 @@ class SnippetManager(object):
self._teardown_inner_state()
def _jump(self, backwards=False):
"""Helper method that does the actual jump."""
jumped = False
# If next tab has length 1 and the distance between itself and
# self._ctab is 1 then there is 1 less CursorMove events. We
# cannot ignore next movement in such case.
ntab_short_and_near = False
if self._cs:
ntab = self._cs.select_next_tab(backwards)
if ntab:
if self._cs.snippet.has_option('s'):
lineno = _vim.buf.cursor.line
_vim.buf[lineno] = _vim.buf[lineno].rstrip()
_vim.select(ntab.start, ntab.end)
jumped = True
if (self._ctab is not None
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()
# we need to set 'onemore' there, because of limitations of the vim
# API regarding cursor movements; without that test
# 'CanExpandAnonSnippetInJumpActionWhileSelected' will fail
with _vim.toggle_opt('ve', 'onemore'):
"""Helper method that does the actual jump."""
jumped = False
# We need to remember current snippets stack here because of
# post-jump action on the last tabstop should be able to access
# snippet instance which is ended just now.
stack_for_post_jump = self._csnippets[:]
# If next tab has length 1 and the distance between itself and
# self._ctab is 1 then there is 1 less CursorMove events. We
# cannot ignore next movement in such case.
ntab_short_and_near = False
if self._cs:
snippet_for_action = self._cs
elif stack_for_post_jump:
snippet_for_action = stack_for_post_jump[-1]
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)
self._ctab = ntab
if jumped:
self._vstate.remember_position()
self._vstate.remember_unnamed_register(self._ctab.current_text)
if not ntab_short_and_near:
self._ignore_movements = True
snippet_for_action = None
if self._cs:
ntab = self._cs.select_next_tab(backwards)
if ntab:
if self._cs.snippet.has_option('s'):
lineno = _vim.buf.cursor.line
_vim.buf[lineno] = _vim.buf[lineno].rstrip()
_vim.select(ntab.start, ntab.end)
jumped = True
if (self._ctab is not None
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
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 not ntab_short_and_near:
self._ignore_movements = True
if len(stack_for_post_jump) > 0 and ntab is not None:
with use_proxy_buffer(stack_for_post_jump, self._vstate):
snippet_for_action.snippet.do_post_jump(
ntab.number,
-1 if backwards else 1,
stack_for_post_jump,
snippet_for_action
)
return jumped
def _leaving_insert_mode(self):
@ -557,42 +595,77 @@ class SnippetManager(object):
done with it."""
self._setup_inner_state()
self._snip_expanded_in_action = False
# Adjust before, maybe the trigger is not the complete word
text_before = before
if snippet.matched:
text_before = before[:-len(snippet.matched)]
if self._cs:
start = Position(_vim.buf.cursor.line, len(text_before))
end = Position(_vim.buf.cursor.line, len(before))
with use_proxy_buffer(self._csnippets, self._vstate):
with self._action_context():
cursor_set_in_action = snippet.do_pre_expand(
self._visual_content.text,
self._csnippets
)
# It could be that our trigger contains the content of TextObjects
# in our containing snippet. If this is indeed the case, we have to
# make sure that those are properly killed. We do this by
# pretending that the user deleted and retyped the text that our
# trigger matched.
edit_actions = [
('D', start.line, start.col, snippet.matched),
('I', start.line, start.col, snippet.matched),
]
self._csnippets[0].replay_user_edits(edit_actions)
if cursor_set_in_action:
text_before = _vim.buf.line_till_cursor
before = _vim.buf.line_till_cursor
si = snippet.launch(text_before, self._visual_content,
self._cs.find_parent_for_new_to(start), start, end)
else:
start = Position(_vim.buf.cursor.line, len(text_before))
end = Position(_vim.buf.cursor.line, len(before))
si = snippet.launch(text_before, self._visual_content,
None, start, end)
with suspend_proxy_edits():
if self._cs:
start = Position(_vim.buf.cursor.line, len(text_before))
end = Position(_vim.buf.cursor.line, len(before))
self._visual_content.reset()
self._csnippets.append(si)
# If cursor is set in pre-action, then action was modified
# cursor line, in that case we do not need to do any edits, it
# can break snippet
if not cursor_set_in_action:
# It could be that our trigger contains the content of
# TextObjects in our containing snippet. If this is indeed
# the case, we have to make sure that those are properly
# killed. We do this by pretending that the user deleted
# and retyped the text that our trigger matched.
edit_actions = [
('D', start.line, start.col, snippet.matched),
('I', start.line, start.col, snippet.matched),
]
self._csnippets[0].replay_user_edits(edit_actions)
si.update_textobjects()
si = snippet.launch(text_before, self._visual_content,
self._cs.find_parent_for_new_to(start),
start, end
)
else:
start = Position(_vim.buf.cursor.line, len(text_before))
end = Position(_vim.buf.cursor.line, len(before))
si = snippet.launch(text_before, self._visual_content,
None, start, end)
self._vstate.remember_buffer(self._csnippets[0])
self._visual_content.reset()
self._csnippets.append(si)
si.update_textobjects()
with use_proxy_buffer(self._csnippets, self._vstate):
with self._action_context():
snippet.do_post_expand(
si._start, si._end, self._csnippets
)
self._vstate.remember_buffer(self._csnippets[0])
if not self._snip_expanded_in_action:
self._jump()
elif self._cs.current_text != '':
self._jump()
else:
self._current_snippet_is_done()
if self._inside_action:
self._snip_expanded_in_action = True
self._jump()
def _try_expand(self):
"""Try to expand a snippet in the current place."""
@ -682,3 +755,17 @@ class SnippetManager(object):
if not os.path.exists(dirname):
os.makedirs(dirname)
return file_to_edit
@contextmanager
def _action_context(self):
try:
old_flag = self._inside_action
self._inside_action = True
yield
finally:
self._inside_action = old_flag
UltiSnips_Manager = SnippetManager( # pylint:disable=invalid-name
vim.eval('g:UltiSnipsExpandTrigger'),
vim.eval('g:UltiSnipsJumpForwardTrigger'),
vim.eval('g:UltiSnipsJumpBackwardTrigger'))

View File

@ -178,6 +178,8 @@ class EditableTextObject(TextObject):
for children in self._editable_children:
if children._start <= pos < children._end:
return children.find_parent_for_new_to(pos)
if children._start == pos and pos == children._end:
return children.find_parent_for_new_to(pos)
return self
###############################
@ -222,7 +224,8 @@ class EditableTextObject(TextObject):
else:
child._do_edit(cmd, ctab)
return
elif ((pos < child._start and child._end <= delend) or
elif ((pos < child._start and child._end <= delend and
child.start < delend) or
(pos <= child._start and child._end < delend)):
# Case: this deletion removes the child
to_kill.add(child)

View File

@ -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
import UltiSnips.snippet_manager
class _Tabs(object):
@ -30,6 +31,54 @@ class _Tabs(object):
_VisualContent = namedtuple('_VisualContent', ['mode', 'text'])
class SnippetUtilForAction(dict):
def __init__(self, *args, **kwargs):
super(SnippetUtilForAction, self).__init__(*args, **kwargs)
self.__dict__ = self
def expand_anon(self, *args, **kwargs):
UltiSnips.snippet_manager.UltiSnips_Manager.expand_anon(
*args, **kwargs
)
self.cursor.preserve()
class SnippetUtilCursor(object):
def __init__(self, cursor):
self._cursor = [cursor[0] - 1, cursor[1]]
self._set = False
def preserve(self):
self._set = True
self._cursor = [
_vim.buf.cursor[0],
_vim.buf.cursor[1],
]
def is_set(self):
return self._set
def set(self, line, column):
self.__setitem__(0, line)
self.__setitem__(1, column)
def to_vim_cursor(self):
return (self._cursor[0] + 1, self._cursor[1])
def __getitem__(self, index):
return self._cursor[index]
def __setitem__(self, index, value):
self._set = True
self._cursor[index] = value
def __len__(self):
return 2
def __str__(self):
return str((self._cursor[0], self._cursor[1]))
class SnippetUtil(object):
"""Provides easy access to indentation, etc.
@ -38,11 +87,12 @@ class SnippetUtil(object):
"""
def __init__(self, initial_indent, vmode, vtext):
def __init__(self, initial_indent, vmode, vtext, context):
self._ind = IndentUtil()
self._visual = _VisualContent(vmode, vtext)
self._initial_indent = self._ind.indent_to_spaces(initial_indent)
self._reset('')
self._context = context
def _reset(self, cur):
"""Gets the snippet ready for another update.
@ -149,6 +199,10 @@ class SnippetUtil(object):
"""Content of visual expansions."""
return self._visual
@property
def context(self):
return self._context
def opt(self, option, default=None): # pylint:disable=no-self-use
"""Gets a Vim variable."""
if _vim.eval("exists('%s')" % option) == '1':
@ -186,10 +240,11 @@ class PythonCode(NoneditableTextObject):
self._locals = snippet.locals
text = snippet.visual_content.text
mode = snippet.visual_content.mode
context = snippet.context
break
except AttributeError:
snippet = snippet._parent # pylint:disable=protected-access
self._snip = SnippetUtil(token.indent, mode, text)
self._snip = SnippetUtil(token.indent, mode, text, context)
self._codes = ((
'import re, os, vim, string, random',

View File

@ -30,6 +30,7 @@ class SnippetInstance(EditableTextObject):
self.snippet = snippet
self._cts = 0
self.context = context
self.locals = {'match': last_re, 'context': context}
self.globals = globals
self.visual_content = visual_content
@ -130,6 +131,9 @@ class SnippetInstance(EditableTextObject):
self._parent = cached_parent
return rv
def get_tabstops(self):
return self._tabstops
class _VimCursor(NoneditableTextObject):

View File

@ -37,5 +37,9 @@ class TabStop(EditableTextObject):
return self._parent is None
def __repr__(self):
try:
text = self.current_text
except IndexError:
text = '<err>'
return 'TabStop(%s,%r->%r,%r)' % (self.number, self._start,
self._end, self.current_text)
self._end, text)

View File

@ -151,6 +151,12 @@ syn match snipPriority "^priority\%(\s.*\|$\)" contains=snipPriorityKeyword disp
syn match snipPriorityKeyword "^priority" contained nextgroup=snipPriorityValue skipwhite display
syn match snipPriorityValue "-\?\d\+" contained display
" Actions {{{3
syn match snipAction "^\(pre_expand\|post_expand\|post_jump\)\%(\s.*\|$\)" contains=snipActionKeyword display
syn match snipActionKeyword "^\(pre_expand\|post_expand\|post_jump\)" contained nextgroup=snipActionValue skipwhite display
syn match snipActionValue '".*"' contained display
" Snippt Clearing {{{2
syn match snipClear "^clearsnippets\%(\s.*\|$\)" contains=snipClearKeyword display
@ -201,6 +207,9 @@ hi def link snipTransformationOptions Operator
hi def link snipPriorityKeyword Keyword
hi def link snipPriorityValue Number
hi def link snipActionKeyword Keyword
hi def link snipActionValue String
hi def link snipClearKeyword Keyword
" }}}1

View File

@ -49,7 +49,7 @@ class ContextSnippets_UseContext(_VimTest):
return "< " + ins + " >"
endglobal
snippet a "desc" "wrap(buffer[line-1])" e
snippet a "desc" "wrap(snip.buffer[snip.line])" e
{ `!p snip.rv = context` }
endsnippet
"""}
@ -59,7 +59,7 @@ class ContextSnippets_UseContext(_VimTest):
class ContextSnippets_SnippetPriority(_VimTest):
files = { 'us/all.snippets': r"""
snippet i "desc" "re.search('err :=', buffer[line-2])" e
snippet i "desc" "re.search('err :=', snip.buffer[snip.line-1])" e
if err != nil {
${1:// pass}
}
@ -119,7 +119,7 @@ class ContextSnippets_ReportErrorOnIndexOutOfRange(_VimTest):
skip_if = lambda self: 'Bug in Neovim.' \
if self.vim_flavor == 'neovim' else None
files = { 'us/all.snippets': r"""
snippet e "desc" "buffer[123]" e
snippet e "desc" "snip.buffer[123]" e
error
endsnippet
"""}
@ -127,3 +127,25 @@ class ContextSnippets_ReportErrorOnIndexOutOfRange(_VimTest):
keys = 'e' + EX
wanted = 'e' + EX
expected_error = r"IndexError: line number out of range"
class ContextSnippets_CursorIsZeroBased(_VimTest):
files = { 'us/all.snippets': r"""
snippet e "desc" "snip.cursor" e
`!p snip.rv = str(snip.context)`
endsnippet
"""}
keys = "e" + EX
wanted = "(2, 1)"
class ContextSnippets_ContextIsClearedBeforeExpand(_VimTest):
files = { 'us/all.snippets': r"""
pre_expand "snip.context = 1 if snip.context is None else 2"
snippet e "desc" w
`!p snip.rv = str(snip.context)`
endsnippet
"""}
keys = "e" + EX + " " + "e" + EX
wanted = "1 1"

View File

@ -69,6 +69,11 @@ class DeleteSnippetInsertion1(_VimTest):
snippets = ('test', r"$1${1/(.*)/(?0::.)/}")
keys = 'test' + EX + ESC + 'u'
wanted = 'test'
class DoNotCrashOnUndoAndJumpInNestedSnippet(_VimTest):
snippets = ('test', r"if $1: $2")
keys = 'test' + EX + 'a' + JF + 'test' + EX + ESC + 'u' + JF
wanted = 'if a: test'
# End: Undo of Snippet insertion #}}}
# Normal mode editing {{{#

348
test/test_SnippetActions.py Normal file
View File

@ -0,0 +1,348 @@
from test.vim_test_case import VimTestCase as _VimTest
from test.constant import *
class SnippetActions_PreActionModifiesBuffer(_VimTest):
files = { 'us/all.snippets': r"""
pre_expand "snip.buffer[snip.line:snip.line] = ['\n']"
snippet a "desc" "True" e
abc
endsnippet
"""}
keys = 'a' + EX
wanted = '\nabc'
class SnippetActions_PostActionModifiesBuffer(_VimTest):
files = { 'us/all.snippets': r"""
post_expand "snip.buffer[snip.line+1:snip.line+1] = ['\n']"
snippet a "desc" "True" e
abc
endsnippet
"""}
keys = 'a' + EX
wanted = 'abc\n'
class SnippetActions_ErrorOnBufferModificationThroughCommand(_VimTest):
files = { 'us/all.snippets': r"""
pre_expand "vim.command('normal O')"
snippet a "desc" "True" e
abc
endsnippet
"""}
keys = 'a' + EX
expected_error = 'changes are untrackable'
class SnippetActions_ErrorOnModificationSnippetLine(_VimTest):
files = { 'us/all.snippets': r"""
post_expand "vim.command('normal dd')"
snippet i "desc" "True" e
if:
$1
endsnippet
"""}
keys = 'i' + EX
expected_error = 'line under the cursor was modified'
class SnippetActions_EnsureIndent(_VimTest):
files = { 'us/all.snippets': r"""
pre_expand "snip.buffer[snip.line] = ' '*4; snip.cursor[1] = 4"
snippet i "desc" "True" e
if:
$1
endsnippet
"""}
keys = '\ni' + EX + 'i' + EX + 'x'
wanted = """
if:
if:
x"""
class SnippetActions_PostActionCanUseSnippetRange(_VimTest):
files = { 'us/all.snippets': r"""
global !p
def ensure_newlines(start, end):
snip.buffer[start[0]:start[0]] = ['\n'] * 2
snip.buffer[end[0]+1:end[0]+1] = ['\n'] * 1
endglobal
post_expand "ensure_newlines(snip.snippet_start, snip.snippet_end)"
snippet i "desc"
if
$1
else
$2
end
endsnippet
"""}
keys = '\ni' + EX + 'x' + JF + 'y'
wanted = """
if
x
else
y
end
"""
class SnippetActions_CanModifyParentBody(_VimTest):
files = { 'us/all.snippets': r"""
global !p
def ensure_newlines(start, end):
snip.buffer[start[0]:start[0]] = ['\n'] * 2
endglobal
post_expand "ensure_newlines(snip.snippet_start, snip.snippet_end)"
snippet i "desc"
if
$1
else
$2
end
endsnippet
"""}
keys = '\ni' + EX + 'i' + EX + 'x' + JF + 'y' + JF + JF + 'z'
wanted = """
if
if
x
else
y
end
else
z
end"""
class SnippetActions_MoveParentSnippetFromChildInPreAction(_VimTest):
files = { 'us/all.snippets': r"""
global !p
def insert_import():
snip.buffer[2:2] = ['import smthing', '']
endglobal
pre_expand "insert_import()"
snippet p "desc"
print(smthing.traceback())
endsnippet
snippet i "desc"
if
$1
else
$2
end
endsnippet
"""}
keys = 'i' + EX + 'p' + EX + JF + 'z'
wanted = """import smthing
if
print(smthing.traceback())
else
z
end"""
class SnippetActions_CanExpandSnippetInDifferentPlace(_VimTest):
files = { 'us/all.snippets': r"""
global !p
def expand_after_if(snip):
snip.buffer[snip.line] = snip.buffer[snip.line][:snip.column] + \
snip.buffer[snip.line][snip.column+1:]
snip.cursor[1] = snip.buffer[snip.line].index('if ')+3
endglobal
pre_expand "expand_after_if(snip)"
snippet n "append not to if" w
not $0
endsnippet
snippet i "if cond" w
if $1: $2
endsnippet
"""}
keys = 'i' + EX + 'blah' + JF + 'n' + EX + JF + 'pass'
wanted = """if not blah: pass"""
class SnippetActions_MoveVisual(_VimTest):
files = { 'us/all.snippets': r"""
global !p
def extract_method(snip):
del snip.buffer[snip.line]
snip.buffer[len(snip.buffer)-1:len(snip.buffer)-1] = ['']
snip.cursor.set(len(snip.buffer)-2, 0)
endglobal
pre_expand "extract_method(snip)"
snippet n "append not to if" w
def $1:
${VISUAL}
endsnippet
"""}
keys = """
def a:
x()
y()
z()""" + ESC + 'kVk' + EX + 'n' + EX + 'b'
wanted = """
def a:
z()
def b:
x()
y()"""
class SnippetActions_CanMirrorTabStopsOutsideOfSnippet(_VimTest):
files = { 'us/all.snippets': r"""
post_jump "snip.buffer[2] = 'debug({})'.format(snip.tabstops[1].current_text)"
snippet i "desc"
if $1:
$2
endsnippet
"""}
keys = """
---
i""" + EX + "test(some(complex(cond(a))))" + JF + "x"
wanted = """debug(test(some(complex(cond(a)))))
---
if test(some(complex(cond(a)))):
x"""
class SnippetActions_CanExpandAnonSnippetInJumpAction(_VimTest):
files = { 'us/all.snippets': r"""
global !p
def expand_anon(snip):
if snip.tabstop == 0:
snip.expand_anon("a($2, $1)")
endglobal
post_jump "expand_anon(snip)"
snippet i "desc"
if ${1:cond}:
$0
endsnippet
"""}
keys = "i" + EX + "x" + JF + "1" + JF + "2" + JF + ";"
wanted = """if x:
a(2, 1);"""
class SnippetActions_CanExpandAnonSnippetInJumpActionWhileSelected(_VimTest):
files = { 'us/all.snippets': r"""
global !p
def expand_anon(snip):
if snip.tabstop == 0:
snip.expand_anon(" // a($2, $1)")
endglobal
post_jump "expand_anon(snip)"
snippet i "desc"
if ${1:cond}:
${2:pass}
endsnippet
"""}
keys = "i" + EX + "x" + JF + JF + "1" + JF + "2" + JF + ";"
wanted = """if x:
pass // a(2, 1);"""
class SnippetActions_CanUseContextFromContextMatch(_VimTest):
files = { 'us/all.snippets': r"""
pre_expand "snip.buffer[snip.line:snip.line] = [snip.context]"
snippet i "desc" "'some context'" e
body
endsnippet
"""}
keys = "i" + EX
wanted = """some context
body"""
class SnippetActions_CanExpandAnonSnippetOnFirstJump(_VimTest):
files = { 'us/all.snippets': r"""
global !p
def expand_new_snippet_on_first_jump(snip):
if snip.tabstop == 1:
snip.expand_anon("some_check($1, $2, $3)")
endglobal
post_jump "expand_new_snippet_on_first_jump(snip)"
snippet "test" "test new features" "True" bwre
if $1: $2
endsnippet
"""}
keys = "test" + EX + "1" + JF + "2" + JF + "3" + JF + " or 4" + JF + "5"
wanted = """if some_check(1, 2, 3) or 4: 5"""
class SnippetActions_CanExpandAnonOnPreExpand(_VimTest):
files = { 'us/all.snippets': r"""
pre_expand "snip.buffer[snip.line] = ''; snip.expand_anon('totally_different($2, $1)')"
snippet test "test new features" wb
endsnippet
"""}
keys = "test" + EX + "1" + JF + "2" + JF + "3"
wanted = """totally_different(2, 1)3"""
class SnippetActions_CanEvenWrapSnippetInPreAction(_VimTest):
files = { 'us/all.snippets': r"""
pre_expand "snip.buffer[snip.line] = ''; snip.expand_anon('some_wrapper($1): $2')"
snippet test "test new features" wb
wrapme($2, $1)
endsnippet
"""}
keys = "test" + EX + "1" + JF + "2" + JF + "3" + JF + "4"
wanted = """some_wrapper(wrapme(2, 1)3): 4"""
class SnippetActions_CanVisuallySelectFirstPlaceholderInAnonSnippetInPre(_VimTest):
files = { 'us/all.snippets': r"""
pre_expand "snip.buffer[snip.line] = ''; snip.expand_anon('${1:asd}, ${2:blah}')"
snippet test "test new features" wb
endsnippet
"""}
keys = "test" + EX + "1" + JF + "2"
wanted = """1, 2"""
class SnippetActions_UseCorrectJumpActions(_VimTest):
files = { 'us/all.snippets': r"""
post_jump "snip.buffer[-2:-2]=['a' + str(snip.tabstop)]"
snippet a "a" wb
$1 {
$2
}
endsnippet
snippet b "b" wb
bbb
endsnippet
post_jump "snip.buffer[-2:-2]=['c' + str(snip.tabstop)]"
snippet c "c" w
$1 : $2 : $3
endsnippet
"""}
keys = "a" + EX + "1" + JF + "b" + EX + " c" + EX + "2" + JF + "3" + JF + "4" + JF + JF
wanted = """1 {
bbb 2 : 3 : 4
}
a1
a2
c1
c2
c3
c0
a0"""

View File

@ -380,3 +380,29 @@ class TabStop_AdjacentTabStopAddText_ExpectCorrectResult(_VimTest):
snippets = ('test', '[ $1$2 ] $1')
keys = 'test' + EX + 'Hello' + JF + 'World' + JF
wanted = '[ HelloWorld ] Hello'
class TabStop_KeepCorrectJumpListOnOverwriteOfPartOfSnippet(_VimTest):
files = { 'us/all.snippets': r"""
snippet i
ia$1: $2
endsnippet
snippet ia
ia($1, $2)
endsnippet"""}
keys = 'i' + EX + EX + '1' + JF + '2' + JF + ' after' + JF + '3'
wanted = 'ia(1, 2) after: 3'
class TabStop_KeepCorrectJumpListOnOverwriteOfPartOfSnippetRE(_VimTest):
files = { 'us/all.snippets': r"""
snippet i
ia$1: $2
endsnippet
snippet "^ia" "regexp" r
ia($1, $2)
endsnippet"""}
keys = 'i' + EX + EX + '1' + JF + '2' + JF + ' after' + JF + '3'
wanted = 'ia(1, 2) after: 3'

View File

@ -159,7 +159,7 @@ class MySnippetSource(SnippetSource):
return [
UltiSnipsSnippetDefinition(
-100, "blumba", "this is a dynamic snippet", "", "", {}, "blub",
None)
None, {})
]
return []
""")

View File

@ -46,16 +46,16 @@ class VimTestCase(unittest.TestCase, TempFileManager):
# Only checks the output. All work is done in setUp().
wanted = self.text_before + self.wanted + self.text_after
if self.expected_error:
self.assertRegexpMatches(self.output, self.expected_error)
return
for i in range(self.retries):
if self.output != wanted:
if self.output and self.expected_error:
self.assertRegexpMatches(self.output, self.expected_error)
return
if self.output != wanted or self.output is None:
# Redo this, but slower
self.sleeptime += 0.15
self.tearDown()
self.setUp()
self.assertEqual(self.output, wanted)
self.assertMultiLineEqual(self.output, wanted)
def _extra_vim_config(self, vim_config):
"""Adds extra lines to the vim_config list."""