pre/post-expand and post-jump actions

This commit is contained in:
Stanislav Seletskiy 2015-05-05 00:17:58 +06:00
parent 67fbdb2ad8
commit 1ca82f76f7
16 changed files with 874 additions and 35 deletions

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|
@ -1417,6 +1421,179 @@ endsnippet
That snippet will expand to 'var1 +=' after line, which begins from 'var1 :='.
4.10 Snippets actions *UltiSnips-snippets-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.
Note: special variable called '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.
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
'buffer', snippet expansion position will be automatically adjusted.
If cursor line (where trigger was matched) need to be modified, then special
variable named 'new_cursor' must be set to 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 '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 "buffer[line] = ' '*4; new_cursor = (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 buffer[line]; buffer.append(''); new_cursor = (len(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
'buffer', snippet expansion position will be automatically adjusted.
Variables 'snippet_start' and '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: 'snippet_start' and '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 "buffer[snippet_end[0]+1: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
'buffer', snippet expansion position will be automatically adjusted.
Next variables will be also defined in the action code scope:
* 'tabstop' - number of tabstop jumped onto;
* 'jump_direction' - '1' if jumped forward and '-1' otherwise;
* 'tabstops' - list with tabstop objects, see above;
* 'snippet_start' - (line, column) of start of the expanded snippet;
* 'snippet_end' - (line, column) of end of the expanded snippet;
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 tabstop == 0: insert_toc_item(tabstops[1], 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 'new_cursor' should be set to special value named '"keep"', 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
from UltiSnips import UltiSnips_Manager
def insert_method_call(name):
global new_cursor
vim.command('normal G')
UltiSnips_Manager.expand_anon(name + '($1)\n')
new_cursor = 'keep'
endglobal
post_jump "if tabstop == 0: insert_method_call(tabstops[1].current_text)"
snippet d "method declaration" b
def $1():
$2
endsnippet
------------------- SNAP -------------------
==============================================================================
5. UltiSnips and Other Plugins *UltiSnips-other-plugins*

View File

@ -108,6 +108,17 @@ def feedkeys(keys, mode='n'):
Mainly for convenience.
"""
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))
@ -137,13 +148,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 +177,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,100 @@
# coding=utf8
import vim
from UltiSnips.position import Position
from UltiSnips._diff import diff
class VimBufferHelper:
def __init__(self, snippets_stack):
self._snippets_stack = snippets_stack
self._buffer = vim.current.buffer
self._buffer_copy = self._buffer[:]
def is_buffer_changed_outside(self):
if len(self._buffer) != len(self._buffer_copy):
return True
for line_number in range(0, len(self._buffer_copy)):
if self._buffer[line_number] != self._buffer_copy[line_number]:
return True
return False
def validate_buffer(self):
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 `buffer` for' +
'buffer modifications')
def __setitem__(self, key, value):
if isinstance(key, slice):
changes = list(self._get_diff(key.start, key.stop, value))
self._buffer[key.start:key.stop] = value
self._buffer_copy[key.start:key.stop] = map(
lambda line: line.strip('\n'),
value
)
map(self._apply_change, changes)
else:
changes = list(self._get_line_diff(key, self._buffer[key], value))
self._buffer[key] = value
self._buffer_copy[key] = value
map(self._apply_change, changes)
def __getitem__(self, key):
if isinstance(key, slice):
return self._buffer[key.start:key.stop]
else:
return self._buffer[key]
def __len__(self):
return len(self._buffer)
def append(self, line, line_number=-1):
if line_number < 0:
line_number = len(self)
if not isinstance(line, list):
line = [line]
self[line_number:line_number] = 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):
for line_number in range(start, end):
yield ('D', line_number, 0, self._buffer[line_number])
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):
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):
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:
self._snippets_stack[0]._do_edit(change)

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

@ -12,6 +12,8 @@ from UltiSnips.compatibility import as_unicode
from UltiSnips.indent_util import IndentUtil
from UltiSnips.text import escape
from UltiSnips.text_objects import SnippetInstance
from UltiSnips.position import Position
from UltiSnips.buffer_helper import VimBufferHelper
__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,93 @@ 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('holder["result"] = ' + self._context_code)
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
holder = {'result': False}
locals = {
'context': context,
'holder': holder,
'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': (current.window.cursor[0]-1, current.window.cursor[1]-1)
}
locals.update(additional_locals)
exec(code, locals)
return context["match"]
return holder["result"]
def _execute_action(
self,
action,
context,
additional_locals={}
):
mark_to_use = '`'
mark_pos = _vim.get_mark_pos(mark_to_use)
_vim.set_mark_from_pos(mark_to_use, _vim.get_cursor_pos())
cursor_line_before = _vim.buf.line_till_cursor
locals = {
'new_cursor': None,
'context': context,
}
locals.update(additional_locals)
new_cursor, new_context = self._eval_code(
action + "\nholder['result'] = (new_cursor, context)",
locals
)
cursor_set_in_action = False
if new_cursor:
if new_cursor != 'keep':
vim.current.window.cursor = (new_cursor[0]+1, new_cursor[1])
cursor_set_in_action = True
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 "new_cursor" ' +
'variable is not set; either set set "new_cursor" to ' +
'new cursor position, or do not modify cursor line'
)
# restore original mark position
if _vim._is_pos_zero(mark_pos):
_vim.delete_mark(mark_to_use)
else:
_vim.set_mark_from_pos(mark_to_use, mark_pos)
return cursor_set_in_action, new_context
def has_option(self, opt):
"""Check if the named option is set."""
@ -243,6 +310,67 @@ class SnippetDefinition(object):
objects alive inside of Vim."""
raise NotImplementedError()
def do_pre_expand(self, visual_content, snippets_stack):
buffer = VimBufferHelper(snippets_stack)
if 'pre_expand' in self._actions:
locals = {'buffer': buffer, 'visual_content': visual_content}
cursor_set_in_action, new_context = self._execute_action(
self._actions['pre_expand'], self._context, locals
)
self._context = new_context
return buffer, cursor_set_in_action
else:
return buffer, False
def do_post_expand(self, start, end, snippets_stack):
buffer = VimBufferHelper(snippets_stack)
if 'post_expand' in self._actions:
locals = {
'snippet_start': start,
'snippet_end': end,
'buffer': buffer
}
cursor_set_in_action, new_context = self._execute_action(
self._actions['post_expand'], snippets_stack[0].context, locals
)
snippets_stack[0].context = new_context
return buffer, cursor_set_in_action
else:
return buffer, False
def do_post_jump(
self, tabstop_number, jump_direction, snippets_stack
):
buffer = VimBufferHelper(snippets_stack)
if 'post_jump' in self._actions:
start = snippets_stack[0].start
end = snippets_stack[0].end
locals = {
'tabstop': tabstop_number,
'jump_direction': jump_direction,
'tabstops': snippets_stack[0].get_tabstops(),
'snippet_start': start,
'snippet_end': end,
'buffer': buffer
}
cursor_set_in_action, new_context = self._execute_action(
self._actions['post_jump'], snippets_stack[0].context, locals
)
snippets_stack[0].context = new_context
return buffer, cursor_set_in_action
else:
return buffer, (False, None)
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

@ -205,19 +205,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))
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 +278,7 @@ class SnippetManager(object):
if _vim.eval('mode()') not in 'in':
return
if self._ignore_movements:
self._ignore_movements = False
return
@ -432,6 +436,18 @@ class SnippetManager(object):
def _jump(self, backwards=False):
"""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[:]
# we need to set 'onemore' there, because of limitations of the vim
# API regarding cursor movements; without that test
# 'CanExpandAnonSnippetInJumpActionWhileSelected' will fail
old_virtualedit = _vim.eval('&ve')
_vim.command('set ve=onemore')
# 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.
@ -450,18 +466,28 @@ class SnippetManager(object):
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)
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
if len(stack_for_post_jump) > 0 and ntab is not None:
stack_for_post_jump[0].snippet.do_post_jump(
ntab.number,
-1 if backwards else 1,
stack_for_post_jump
)
_vim.command('set ve=' + old_virtualedit)
return jumped
def _leaving_insert_mode(self):
@ -562,6 +588,17 @@ class SnippetManager(object):
if snippet.matched:
text_before = before[:-len(snippet.matched)]
new_buffer, cursor_set_in_action = snippet.do_pre_expand(
self._visual_content.text,
self._csnippets
)
new_buffer.validate_buffer()
if cursor_set_in_action:
text_before = _vim.buf.line_till_cursor
before = _vim.buf.line_till_cursor
if self._cs:
start = Position(_vim.buf.cursor.line, len(text_before))
end = Position(_vim.buf.cursor.line, len(before))
@ -590,6 +627,12 @@ class SnippetManager(object):
si.update_textobjects()
new_buffer, _ = snippet.do_post_expand(
si._start, si._end, self._csnippets
)
new_buffer.validate_buffer()
self._vstate.remember_buffer(self._csnippets[0])
self._jump()

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

@ -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(buffer[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 :=', buffer[line-1])" e
if err != nil {
${1:// pass}
}
@ -127,3 +127,14 @@ 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" "cursor" e
`!p snip.rv = str(context)`
endsnippet
"""}
keys = "e" + EX
wanted = "(2, 0)"

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 {{{#

287
test/test_SnippetActions.py Normal file
View File

@ -0,0 +1,287 @@
from test.vim_test_case import VimTestCase as _VimTest
from test.constant import *
class SnippetActions_PreActionModifiesBuffer(_VimTest):
files = { 'us/all.snippets': r"""
pre_expand "buffer[line: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 "buffer[line+1: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 "buffer[line] = ' '*4; new_cursor = (cursor[0], 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):
buffer[start[0]:start[0]] = ['\n'] * 2
buffer[end[0]+1:end[0]+1] = ['\n'] * 1
endglobal
post_expand "ensure_newlines(snippet_start, 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):
buffer[start[0]:start[0]] = ['\n'] * 2
endglobal
post_expand "ensure_newlines(snippet_start, 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():
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():
global new_cursor
buffer[line] = buffer[line][:column] + buffer[line][column+1:]
new_cursor = (line, buffer[line].index('if ')+3)
endglobal
pre_expand "expand_after_if()"
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():
global new_cursor
del buffer[line]
buffer[len(buffer)-1:len(buffer)-1] = ['']
new_cursor = (len(buffer)-2, 0)
endglobal
pre_expand "extract_method()"
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 "buffer[2] = 'debug({})'.format(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():
if tabstop == 0:
from UltiSnips import UltiSnips_Manager
UltiSnips_Manager.expand_anon("a($2, $1)")
return 'keep'
endglobal
post_jump "new_cursor = expand_anon()"
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():
if tabstop == 0:
from UltiSnips import UltiSnips_Manager
UltiSnips_Manager.expand_anon(" // a($2, $1)")
return 'keep'
endglobal
post_jump "new_cursor = expand_anon()"
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"""
global !p
def expand_anon():
if tabstop == 0:
from UltiSnips import UltiSnips_Manager
UltiSnips_Manager.expand_anon(" // a($2, $1)")
return 'keep'
endglobal
pre_expand "buffer[line:line] = [context]"
snippet i "desc" "'some context'" e
body
endsnippet
"""}
keys = "i" + EX
wanted = """some context
body"""

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:
for i in range(self.retries):
if self.output and self.expected_error:
self.assertRegexpMatches(self.output, self.expected_error)
return
for i in range(self.retries):
if self.output != wanted:
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."""
@ -128,6 +128,7 @@ class VimTestCase(unittest.TestCase, TempFileManager):
vim_config.append('set fileencoding=utf-8')
vim_config.append('set buftype=nofile')
vim_config.append('set shortmess=at')
vim_config.append('set cmdheight=10')
vim_config.append('let @" = ""')
assert EX == "\t" # Otherwise you need to change the next line
vim_config.append('let g:UltiSnipsExpandTrigger="<tab>"')