pre/post-expand and post-jump actions
This commit is contained in:
parent
67fbdb2ad8
commit
1ca82f76f7
@ -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*
|
||||
|
||||
|
@ -108,7 +108,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 +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.
|
||||
|
100
pythonx/UltiSnips/buffer_helper.py
Normal file
100
pythonx/UltiSnips/buffer_helper.py
Normal 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)
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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))
|
||||
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 +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()
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)"
|
||||
|
@ -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
287
test/test_SnippetActions.py
Normal 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"""
|
@ -159,7 +159,7 @@ class MySnippetSource(SnippetSource):
|
||||
return [
|
||||
UltiSnipsSnippetDefinition(
|
||||
-100, "blumba", "this is a dynamic snippet", "", "", {}, "blub",
|
||||
None)
|
||||
None, {})
|
||||
]
|
||||
return []
|
||||
""")
|
||||
|
@ -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."""
|
||||
@ -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>"')
|
||||
|
Loading…
x
Reference in New Issue
Block a user