diff --git a/doc/UltiSnips.txt b/doc/UltiSnips.txt index 2c8e5e4..9d6f88a 100644 --- a/doc/UltiSnips.txt +++ b/doc/UltiSnips.txt @@ -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* diff --git a/pythonx/UltiSnips/_vim.py b/pythonx/UltiSnips/_vim.py index 9665a2b..09cc32b 100644 --- a/pythonx/UltiSnips/_vim.py +++ b/pythonx/UltiSnips/_vim.py @@ -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"\" 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. diff --git a/pythonx/UltiSnips/buffer_helper.py b/pythonx/UltiSnips/buffer_helper.py new file mode 100644 index 0000000..3c114f2 --- /dev/null +++ b/pythonx/UltiSnips/buffer_helper.py @@ -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) diff --git a/pythonx/UltiSnips/position.py b/pythonx/UltiSnips/position.py index c2a68f4..3c9d2a5 100644 --- a/pythonx/UltiSnips/position.py +++ b/pythonx/UltiSnips/position.py @@ -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 diff --git a/pythonx/UltiSnips/snippet/definition/_base.py b/pythonx/UltiSnips/snippet/definition/_base.py index 225807a..8a9b563 100644 --- a/pythonx/UltiSnips/snippet/definition/_base.py +++ b/pythonx/UltiSnips/snippet/definition/_base.py @@ -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. diff --git a/pythonx/UltiSnips/snippet/definition/snipmate.py b/pythonx/UltiSnips/snippet/definition/snipmate.py index a77046b..8ab1e2a 100644 --- a/pythonx/UltiSnips/snippet/definition/snipmate.py +++ b/pythonx/UltiSnips/snippet/definition/snipmate.py @@ -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) diff --git a/pythonx/UltiSnips/snippet/source/file/_common.py b/pythonx/UltiSnips/snippet/source/file/_common.py index 82ba603..ec4be60 100644 --- a/pythonx/UltiSnips/snippet/source/file/_common.py +++ b/pythonx/UltiSnips/snippet/source/file/_common.py @@ -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) diff --git a/pythonx/UltiSnips/snippet/source/file/ultisnips.py b/pythonx/UltiSnips/snippet/source/file/ultisnips.py index 0383997..2005ebd 100644 --- a/pythonx/UltiSnips/snippet/source/file/ultisnips.py +++ b/pythonx/UltiSnips/snippet/source/file/ultisnips.py @@ -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) diff --git a/pythonx/UltiSnips/snippet_manager.py b/pythonx/UltiSnips/snippet_manager.py index 69ef69b..7112f44 100644 --- a/pythonx/UltiSnips/snippet_manager.py +++ b/pythonx/UltiSnips/snippet_manager.py @@ -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() diff --git a/pythonx/UltiSnips/text_objects/_snippet_instance.py b/pythonx/UltiSnips/text_objects/_snippet_instance.py index 8b8e222..e3e3158 100644 --- a/pythonx/UltiSnips/text_objects/_snippet_instance.py +++ b/pythonx/UltiSnips/text_objects/_snippet_instance.py @@ -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): diff --git a/syntax/snippets.vim b/syntax/snippets.vim index 94e1458..3a044ce 100644 --- a/syntax/snippets.vim +++ b/syntax/snippets.vim @@ -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 diff --git a/test/test_ContextSnippets.py b/test/test_ContextSnippets.py index fc4fe9d..8d35a77 100644 --- a/test/test_ContextSnippets.py +++ b/test/test_ContextSnippets.py @@ -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)" diff --git a/test/test_Editing.py b/test/test_Editing.py index 518a9ec..06f4a7a 100644 --- a/test/test_Editing.py +++ b/test/test_Editing.py @@ -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 {{{# diff --git a/test/test_SnippetActions.py b/test/test_SnippetActions.py new file mode 100644 index 0000000..2bf5609 --- /dev/null +++ b/test/test_SnippetActions.py @@ -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""" diff --git a/test/test_UltiSnipFunc.py b/test/test_UltiSnipFunc.py index 6568c54..6e6a06e 100644 --- a/test/test_UltiSnipFunc.py +++ b/test/test_UltiSnipFunc.py @@ -159,7 +159,7 @@ class MySnippetSource(SnippetSource): return [ UltiSnipsSnippetDefinition( -100, "blumba", "this is a dynamic snippet", "", "", {}, "blub", - None) + None, {}) ] return [] """) diff --git a/test/vim_test_case.py b/test/vim_test_case.py index ec888f4..6caa78b 100644 --- a/test/vim_test_case.py +++ b/test/vim_test_case.py @@ -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=""')