From 1ca82f76f7a4e7e9beac77f74bda99457bbc8286 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Tue, 5 May 2015 00:17:58 +0600 Subject: [PATCH 01/19] pre/post-expand and post-jump actions --- doc/UltiSnips.txt | 177 +++++++++++ pythonx/UltiSnips/_vim.py | 45 ++- pythonx/UltiSnips/buffer_helper.py | 100 ++++++ pythonx/UltiSnips/position.py | 10 + pythonx/UltiSnips/snippet/definition/_base.py | 150 ++++++++- .../UltiSnips/snippet/definition/snipmate.py | 2 +- .../UltiSnips/snippet/source/file/_common.py | 9 + .../snippet/source/file/ultisnips.py | 26 +- pythonx/UltiSnips/snippet_manager.py | 57 +++- .../text_objects/_snippet_instance.py | 4 + syntax/snippets.vim | 9 + test/test_ContextSnippets.py | 15 +- test/test_Editing.py | 5 + test/test_SnippetActions.py | 287 ++++++++++++++++++ test/test_UltiSnipFunc.py | 2 +- test/vim_test_case.py | 11 +- 16 files changed, 874 insertions(+), 35 deletions(-) create mode 100644 pythonx/UltiSnips/buffer_helper.py create mode 100644 test/test_SnippetActions.py 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=""') From 760fd25e47108ef344e637ad0ca4c51125d81d9d Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Wed, 3 Jun 2015 11:57:17 +0600 Subject: [PATCH 02/19] fix python3 compatibility --- pythonx/UltiSnips/buffer_helper.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pythonx/UltiSnips/buffer_helper.py b/pythonx/UltiSnips/buffer_helper.py index 3c114f2..c905156 100644 --- a/pythonx/UltiSnips/buffer_helper.py +++ b/pythonx/UltiSnips/buffer_helper.py @@ -35,12 +35,13 @@ class VimBufferHelper: 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) + + for change in changes: + self._apply_change(change) def __getitem__(self, key): if isinstance(key, slice): From 7ead6fa1784e63ecfba8a3afc299d111a7300a62 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Mon, 8 Jun 2015 16:30:45 +0600 Subject: [PATCH 03/19] detect buffer changes via changedtick --- pythonx/UltiSnips/buffer_helper.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/pythonx/UltiSnips/buffer_helper.py b/pythonx/UltiSnips/buffer_helper.py index c905156..27d8b3d 100644 --- a/pythonx/UltiSnips/buffer_helper.py +++ b/pythonx/UltiSnips/buffer_helper.py @@ -8,17 +8,10 @@ class VimBufferHelper: def __init__(self, snippets_stack): self._snippets_stack = snippets_stack self._buffer = vim.current.buffer - self._buffer_copy = self._buffer[:] + self._change_tick = int(vim.eval("b:changedtick")) 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 + return self._change_tick != int(vim.eval("b:changedtick")) def validate_buffer(self): if self.is_buffer_changed_outside(): @@ -31,14 +24,11 @@ class VimBufferHelper: 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 - ) else: changes = list(self._get_line_diff(key, self._buffer[key], value)) self._buffer[key] = value - self._buffer_copy[key] = value + + self._change_tick += 1 for change in changes: self._apply_change(change) From 191ebd8e8b45a98109f3bc60233c32591759bc31 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Mon, 8 Jun 2015 18:28:21 +0600 Subject: [PATCH 04/19] migrate to snip.* namespace --- pythonx/UltiSnips/snippet/definition/_base.py | 53 +++++++-------- .../UltiSnips/text_objects/_python_code.py | 42 ++++++++++++ test/test_ContextSnippets.py | 10 +-- test/test_SnippetActions.py | 67 ++++++++----------- 4 files changed, 102 insertions(+), 70 deletions(-) diff --git a/pythonx/UltiSnips/snippet/definition/_base.py b/pythonx/UltiSnips/snippet/definition/_base.py index 8a9b563..9b2a96a 100644 --- a/pythonx/UltiSnips/snippet/definition/_base.py +++ b/pythonx/UltiSnips/snippet/definition/_base.py @@ -12,6 +12,7 @@ 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.text_objects._python_code import SnippetUtilForAction, SnippetUtilCursor from UltiSnips.position import Position from UltiSnips.buffer_helper import VimBufferHelper @@ -91,7 +92,9 @@ class SnippetDefinition(object): if len(vim.current.buffer) == 1 and vim.current.buffer[0] == "": return - return self._eval_code('holder["result"] = ' + self._context_code) + return self._eval_code('snip.context = ' + self._context_code, { + 'context': None + }).context def _eval_code(self, code, additional_locals={}): code = "\n".join([ @@ -105,19 +108,20 @@ class SnippetDefinition(object): holder = {'result': False} locals = { - 'holder': holder, 'window': current.window, 'buffer': current.buffer, 'line': current.window.cursor[0]-1, 'column': current.window.cursor[1]-1, - 'cursor': (current.window.cursor[0]-1, current.window.cursor[1]-1) + 'cursor': SnippetUtilCursor(current.window.cursor) } locals.update(additional_locals) - exec(code, locals) + snip = SnippetUtilForAction(locals) - return holder["result"] + exec(code, {'snip': snip}) + + return snip def _execute_action( self, @@ -133,22 +137,15 @@ class SnippetDefinition(object): 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 - ) + snip = self._eval_code(action, 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 + if snip.cursor.is_set(): + vim.current.window.cursor = snip.cursor.to_vim_cursor() else: new_mark_pos = _vim.get_mark_pos(mark_to_use) @@ -163,8 +160,8 @@ class SnippetDefinition(object): if cursor_invalid: raise RuntimeError( - 'line under the cursor was modified, but "new_cursor" ' + - 'variable is not set; either set set "new_cursor" to ' + + 'line under the cursor was modified, but "snip.cursor" ' + + 'variable is not set; either set set "snip.cursor" to ' + 'new cursor position, or do not modify cursor line' ) @@ -173,7 +170,8 @@ class SnippetDefinition(object): _vim.delete_mark(mark_to_use) else: _vim.set_mark_from_pos(mark_to_use, mark_pos) - return cursor_set_in_action, new_context + + return snip def has_option(self, opt): """Check if the named option is set.""" @@ -315,13 +313,13 @@ class SnippetDefinition(object): if 'pre_expand' in self._actions: locals = {'buffer': buffer, 'visual_content': visual_content} - cursor_set_in_action, new_context = self._execute_action( + snip = self._execute_action( self._actions['pre_expand'], self._context, locals ) - self._context = new_context + self._context = snip.context - return buffer, cursor_set_in_action + return buffer, snip.cursor.is_set() else: return buffer, False @@ -334,13 +332,13 @@ class SnippetDefinition(object): 'buffer': buffer } - cursor_set_in_action, new_context = self._execute_action( + snip = self._execute_action( self._actions['post_expand'], snippets_stack[0].context, locals ) - snippets_stack[0].context = new_context + snippets_stack[0].context = snip.context - return buffer, cursor_set_in_action + return buffer, snip.cursor.is_set() else: return buffer, False @@ -351,6 +349,7 @@ class SnippetDefinition(object): if 'post_jump' in self._actions: start = snippets_stack[0].start end = snippets_stack[0].end + locals = { 'tabstop': tabstop_number, 'jump_direction': jump_direction, @@ -360,13 +359,13 @@ class SnippetDefinition(object): 'buffer': buffer } - cursor_set_in_action, new_context = self._execute_action( + snip = self._execute_action( self._actions['post_jump'], snippets_stack[0].context, locals ) - snippets_stack[0].context = new_context + snippets_stack[0].context = snip.context - return buffer, cursor_set_in_action + return buffer, snip.cursor.is_set() else: return buffer, (False, None) diff --git a/pythonx/UltiSnips/text_objects/_python_code.py b/pythonx/UltiSnips/text_objects/_python_code.py index 5660d6e..db3c060 100644 --- a/pythonx/UltiSnips/text_objects/_python_code.py +++ b/pythonx/UltiSnips/text_objects/_python_code.py @@ -30,6 +30,48 @@ class _Tabs(object): _VisualContent = namedtuple('_VisualContent', ['mode', 'text']) +class SnippetUtilForAction(dict): + def __init__(self, *args, **kwargs): + super(SnippetUtilForAction, self).__init__(*args, **kwargs) + self.__dict__ = self + + +class SnippetUtilCursor(object): + def __init__(self, cursor): + self._cursor = [cursor[0] - 1, cursor[1]] + self._set = False + + def preserve(self): + self._set = True + self._cursor = [ + _vim.buf.cursor[0], + _vim.buf.cursor[1], + ] + + def is_set(self): + return self._set + + def set(self, line, column): + self.__setitem__(0, line) + self.__setitem__(1, column) + + def to_vim_cursor(self): + return (self._cursor[0] + 1, self._cursor[1]) + + def __getitem__(self, index): + return self._cursor[index] + + def __setitem__(self, index, value): + self._set = True + self._cursor[index] = value + + def __len__(self): + return 2 + + def __str__(self): + return str((self._cursor[0], self._cursor[1])) + + class SnippetUtil(object): """Provides easy access to indentation, etc. diff --git a/test/test_ContextSnippets.py b/test/test_ContextSnippets.py index 8d35a77..307438f 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])" e + snippet a "desc" "wrap(snip.buffer[snip.line])" e { `!p snip.rv = context` } endsnippet """} @@ -59,7 +59,7 @@ class ContextSnippets_UseContext(_VimTest): class ContextSnippets_SnippetPriority(_VimTest): files = { 'us/all.snippets': r""" - snippet i "desc" "re.search('err :=', buffer[line-1])" e + snippet i "desc" "re.search('err :=', snip.buffer[snip.line-1])" e if err != nil { ${1:// pass} } @@ -119,7 +119,7 @@ class ContextSnippets_ReportErrorOnIndexOutOfRange(_VimTest): skip_if = lambda self: 'Bug in Neovim.' \ if self.vim_flavor == 'neovim' else None files = { 'us/all.snippets': r""" - snippet e "desc" "buffer[123]" e + snippet e "desc" "snip.buffer[123]" e error endsnippet """} @@ -131,10 +131,10 @@ class ContextSnippets_ReportErrorOnIndexOutOfRange(_VimTest): class ContextSnippets_CursorIsZeroBased(_VimTest): files = { 'us/all.snippets': r""" - snippet e "desc" "cursor" e + snippet e "desc" "snip.cursor" e `!p snip.rv = str(context)` endsnippet """} keys = "e" + EX - wanted = "(2, 0)" + wanted = "(2, 1)" diff --git a/test/test_SnippetActions.py b/test/test_SnippetActions.py index 2bf5609..d29a719 100644 --- a/test/test_SnippetActions.py +++ b/test/test_SnippetActions.py @@ -4,7 +4,7 @@ from test.constant import * class SnippetActions_PreActionModifiesBuffer(_VimTest): files = { 'us/all.snippets': r""" - pre_expand "buffer[line:line] = ['\n']" + pre_expand "snip.buffer[snip.line:snip.line] = ['\n']" snippet a "desc" "True" e abc endsnippet @@ -15,7 +15,7 @@ class SnippetActions_PreActionModifiesBuffer(_VimTest): class SnippetActions_PostActionModifiesBuffer(_VimTest): files = { 'us/all.snippets': r""" - post_expand "buffer[line+1:line+1] = ['\n']" + post_expand "snip.buffer[snip.line+1:snip.line+1] = ['\n']" snippet a "desc" "True" e abc endsnippet @@ -48,7 +48,7 @@ class SnippetActions_ErrorOnModificationSnippetLine(_VimTest): class SnippetActions_EnsureIndent(_VimTest): files = { 'us/all.snippets': r""" - pre_expand "buffer[line] = ' '*4; new_cursor = (cursor[0], 4)" + pre_expand "snip.buffer[snip.line] = ' '*4; snip.cursor[1] = 4" snippet i "desc" "True" e if: $1 @@ -65,11 +65,11 @@ 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 + snip.buffer[start[0]:start[0]] = ['\n'] * 2 + snip.buffer[end[0]+1:end[0]+1] = ['\n'] * 1 endglobal - post_expand "ensure_newlines(snippet_start, snippet_end)" + post_expand "ensure_newlines(snip.snippet_start, snip.snippet_end)" snippet i "desc" if $1 @@ -94,10 +94,10 @@ class SnippetActions_CanModifyParentBody(_VimTest): files = { 'us/all.snippets': r""" global !p def ensure_newlines(start, end): - buffer[start[0]:start[0]] = ['\n'] * 2 + snip.buffer[start[0]:start[0]] = ['\n'] * 2 endglobal - post_expand "ensure_newlines(snippet_start, snippet_end)" + post_expand "ensure_newlines(snip.snippet_start, snip.snippet_end)" snippet i "desc" if $1 @@ -127,7 +127,7 @@ class SnippetActions_MoveParentSnippetFromChildInPreAction(_VimTest): files = { 'us/all.snippets': r""" global !p def insert_import(): - buffer[2:2] = ['import smthing', ''] + snip.buffer[2:2] = ['import smthing', ''] endglobal pre_expand "insert_import()" @@ -156,13 +156,13 @@ 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) + def expand_after_if(snip): + snip.buffer[snip.line] = snip.buffer[snip.line][:snip.column] + \ + snip.buffer[snip.line][snip.column+1:] + snip.cursor[1] = snip.buffer[snip.line].index('if ')+3 endglobal - pre_expand "expand_after_if()" + pre_expand "expand_after_if(snip)" snippet n "append not to if" w not $0 endsnippet @@ -178,14 +178,13 @@ class SnippetActions_CanExpandSnippetInDifferentPlace(_VimTest): 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) + def extract_method(snip): + del snip.buffer[snip.line] + snip.buffer[len(snip.buffer)-1:len(snip.buffer)-1] = [''] + snip.cursor.set(len(snip.buffer)-2, 0) endglobal - pre_expand "extract_method()" + pre_expand "extract_method(snip)" snippet n "append not to if" w def $1: ${VISUAL} @@ -210,7 +209,7 @@ def b: class SnippetActions_CanMirrorTabStopsOutsideOfSnippet(_VimTest): files = { 'us/all.snippets': r""" - post_jump "buffer[2] = 'debug({})'.format(tabstops[1].current_text)" + post_jump "snip.buffer[2] = 'debug({})'.format(snip.tabstops[1].current_text)" snippet i "desc" if $1: $2 @@ -228,14 +227,14 @@ if test(some(complex(cond(a)))): class SnippetActions_CanExpandAnonSnippetInJumpAction(_VimTest): files = { 'us/all.snippets': r""" global !p - def expand_anon(): - if tabstop == 0: + def expand_anon(snip): + if snip.tabstop == 0: from UltiSnips import UltiSnips_Manager UltiSnips_Manager.expand_anon("a($2, $1)") - return 'keep' + snip.cursor.preserve() endglobal - post_jump "new_cursor = expand_anon()" + post_jump "expand_anon(snip)" snippet i "desc" if ${1:cond}: $0 @@ -249,14 +248,14 @@ class SnippetActions_CanExpandAnonSnippetInJumpAction(_VimTest): class SnippetActions_CanExpandAnonSnippetInJumpActionWhileSelected(_VimTest): files = { 'us/all.snippets': r""" global !p - def expand_anon(): - if tabstop == 0: + def expand_anon(snip): + if snip.tabstop == 0: from UltiSnips import UltiSnips_Manager UltiSnips_Manager.expand_anon(" // a($2, $1)") - return 'keep' + snip.cursor.preserve() endglobal - post_jump "new_cursor = expand_anon()" + post_jump "expand_anon(snip)" snippet i "desc" if ${1:cond}: ${2:pass} @@ -269,15 +268,7 @@ class SnippetActions_CanExpandAnonSnippetInJumpActionWhileSelected(_VimTest): 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]" + pre_expand "snip.buffer[snip.line:snip.line] = [snip.context]" snippet i "desc" "'some context'" e body endsnippet From 972305725f5796774e0ac0ed30a47dc1ef5bc1ab Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Wed, 10 Jun 2015 23:32:36 +0600 Subject: [PATCH 05/19] fix expan_anon in pre-action, snip object, fixes --- doc/UltiSnips.txt | 90 ++++----- pythonx/UltiSnips/_vim.py | 21 +++ pythonx/UltiSnips/buffer_helper.py | 63 ++++++- pythonx/UltiSnips/snippet/definition/_base.py | 94 +++++----- pythonx/UltiSnips/snippet_manager.py | 172 +++++++++--------- pythonx/UltiSnips/text_objects/_base.py | 5 +- .../UltiSnips/text_objects/_python_code.py | 15 +- pythonx/UltiSnips/text_objects/_tabstop.py | 6 +- test/test_ContextSnippets.py | 2 +- test/test_SnippetActions.py | 43 ++++- test/test_TabStop.py | 26 +++ 11 files changed, 337 insertions(+), 200 deletions(-) diff --git a/doc/UltiSnips.txt b/doc/UltiSnips.txt index 9d6f88a..dee9199 100644 --- a/doc/UltiSnips.txt +++ b/doc/UltiSnips.txt @@ -1347,17 +1347,21 @@ be wrapped with double-quotes. The following python modules are automatically imported into the scope before 'expression' is evaluated: 're', 'os', 'vim', 'string', 'random'. -Also, the following variables are defined: - 'window' - alias for 'vim.current.window' - 'buffer' - alias for 'vim.current.window.buffer' - 'cursor' - alias for 'vim.current.cursor' - 'line' and 'column' - aliases for cursor position +Global variable `snip` will be available with following properties: + 'snip.window' - alias for 'vim.current.window' + 'snip.buffer' - alias for 'vim.current.window.buffer' + 'snip.cursor' - cursor object, which behaves like + 'vim.current.window.cursor', but zero-indexed and with following + additional methods: + - 'preserve()' - special method for executing pre/post/jump actions; + - 'set(line, column)' - sets cursor to specified line and column; + - 'to_vim_cursor()' - returns 1-indexed cursor, suitable for assigning + to 'vim.current.window.cursor'; + 'snip.line' and 'snip.column' - aliases for cursor position (zero-indexed); -Keep in mind, that lines in vim numbered from 1, and lists in python starts -from 0, so to access the current line you need to use 'line-1'. ------------------- SNIP ------------------- -snippet r "return" "re.match('^\s+if err ', buffer[line-2])" be +snippet r "return" "re.match('^\s+if err ', snip.buffer[snip.line-1])" be return err endsnippet ------------------- SNAP ------------------- @@ -1375,7 +1379,7 @@ if $1 { } endsnippet -snippet i "if err != nil" "re.match('^\s+[^=]*err\s*:?=', buffer[line-2])" be +snippet i "if err != nil" "re.match('^\s+[^=]*err\s*:?=', snip.buffer[snip.line-1])" be if err != nil { $1 } @@ -1394,7 +1398,7 @@ global !p import my_utils endglobal -snippet , "return ..., nil/err" "my_utils.is_return_argument(buffer, line, column)" ie +snippet , "return ..., nil/err" "my_utils.is_return_argument(snip)" ie , `!p if my_utils.is_in_err_condition(): snip.rv = "err" else: @@ -1409,19 +1413,19 @@ part of custom python module which is called 'my_utils' in this example. Context condition can return any value which python can use as condition in it's 'if' statement, and if it's considered 'True', then snippet will be -expanded. The evaluated value of 'condition' is available in the 'context' +expanded. The evaluated value of 'condition' is available in the 'snip.context' variable inside the snippet: ------------------- SNIP ------------------- -snippet + "var +=" "re.match('\s*(.*?)\s*:?=', buffer[line-2])" ie -`!p snip.rv = context.group(1)` += $1 +snippet + "var +=" "re.match('\s*(.*?)\s*:?=', snip.buffer[snip.line-1])" ie +`!p snip.rv = snip.context.group(1)` += $1 endsnippet ------------------- SNAP ------------------- That snippet will expand to 'var1 +=' after line, which begins from 'var1 :='. -4.10 Snippets actions *UltiSnips-snippets-actions* +4.10 Snippets actions *UltiSnips-snippet-actions* --------------------- Snippet actions is an arbitrary python code which can be executed at specific @@ -1439,7 +1443,7 @@ 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 +Note: special variable called 'snip.buffer' should be used for all buffer modifications. Not 'vim.current.buffer' and not 'vim.command("...")', because of in that case UltiSnips will not be able to track changes buffer from actions. @@ -1458,14 +1462,14 @@ Pre-expand action declared as follows: > endsnippet Buffer can be modified in pre-expand action code through variable called -'buffer', snippet expansion position will be automatically adjusted. +'snip.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. +variable method 'snip.cursor.set(line, column)' must be called with the +desired cursor position. In that case UltiSnips will not remove any matched +trigger text and it should be done manually in action code. -To addition to the scope variables defined above 'visual_content' will be also +To addition to the scope variables defined above 'snip.visual_content' will be also declared and will contain text that was selected before snippet expansion (similar to $VISUAL placeholder). @@ -1473,7 +1477,7 @@ 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)" +pre_expand "snip.buffer[snip.line] = ' '*4; snip.cursor.set(line, 4)" snippet d def $1(): $0 @@ -1484,7 +1488,7 @@ 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)" +pre_expand "del snip.buffer[snip.line]; snip.buffer.append(''); snip.cursor.set(len(snip.buffer)-1, 0)" snippet x def $1(): ${2:${VISUAL}} @@ -1504,13 +1508,13 @@ Post-expand action declared as follows: > endsnippet Buffer can be modified in post-expand action code through variable called -'buffer', snippet expansion position will be automatically adjusted. +'snip.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)'. +Variables 'snip.snippet_start' and 'snip.snippet_end' will be defined at the +action code scope and will point to positions of the start and end of expanded +snippet accordingly in the form '(line, column)'. -Note: 'snippet_start' and 'snippet_end' will automatically adjust to the +Note: 'snip.snippet_start' and 'snip.snippet_end' will automatically adjust to the correct positions if post-action will insert or delete lines before expansion. Following snippet will expand to method definition and automatically insert @@ -1518,7 +1522,7 @@ 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] = ['']" +post_expand "snip.buffer[snip.snippet_end[0]+1:snip.snippet_end[0]+1] = ['']" snippet d "Description" b def $1(): $2 @@ -1538,14 +1542,15 @@ Jump-expand action declared as follows: > endsnippet Buffer can be modified in post-expand action code through variable called -'buffer', snippet expansion position will be automatically adjusted. +'snip.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; +Next variables and methods will be also defined in the action code scope: +* 'snip.tabstop' - number of tabstop jumped onto; +* 'snip.jump_direction' - '1' if jumped forward and '-1' otherwise; +* 'snip.tabstops' - list with tabstop objects, see above; +* 'snip.snippet_start' - (line, column) of start of the expanded snippet; +* 'snip.snippet_end' - (line, column) of end of the expanded snippet; +* 'snip.expand_anon()' - alias for 'UltiSnips_Manager.expand_anon()'; Tabstop object has several useful properties: * 'start' - (line, column) of the starting position of the tabstop (also @@ -1557,7 +1562,7 @@ 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)" +post_jump "if snip.tabstop == 0: insert_toc_item(snip.tabstops[1], snip.buffer)" snippet s "section" b `!p insert_delimiter_0(snip, t)`$1`!p insert_section_title(snip, t)` `!p insert_delimiter_1(snip, t)` @@ -1569,25 +1574,20 @@ endsnippet 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. +In that case method 'snip.cursor.preserve()' should be called, so UltiSnips +will know that cursor is already at the required position. Following example will insert method call at the end of file after user jump out of method declaration snippet. ------------------- SNIP ------------------- global !p -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' + snip.expand_anon(name + '($1)\n') endglobal -post_jump "if tabstop == 0: insert_method_call(tabstops[1].current_text)" +post_jump "if snip.tabstop == 0: insert_method_call(snip.tabstops[1].current_text)" snippet d "method declaration" b def $1(): $2 diff --git a/pythonx/UltiSnips/_vim.py b/pythonx/UltiSnips/_vim.py index 09cc32b..7bfa6ff 100644 --- a/pythonx/UltiSnips/_vim.py +++ b/pythonx/UltiSnips/_vim.py @@ -12,6 +12,8 @@ from UltiSnips.compatibility import col2byte, byte2col, \ as_unicode, as_vimencoding from UltiSnips.position import Position +from contextlib import contextmanager + class VimBuffer(object): @@ -71,6 +73,25 @@ class VimBuffer(object): vim.current.window.cursor = pos.line + 1, nbyte buf = VimBuffer() # pylint:disable=invalid-name +@contextmanager +def toggle_opt(name, new_value): + old_value = eval('&' + name) + command('set {}={}'.format(name, new_value)) + try: + yield + finally: + command('set {}={}'.format(name, old_value)) + +@contextmanager +def save_mark(name): + old_pos = get_mark_pos(name) + try: + yield + finally: + if _is_pos_zero(old_pos): + delete_mark(name) + else: + set_mark_from_pos(name, old_pos) def escape(inp): """Creates a vim-friendly string from a group of diff --git a/pythonx/UltiSnips/buffer_helper.py b/pythonx/UltiSnips/buffer_helper.py index 27d8b3d..fc55da6 100644 --- a/pythonx/UltiSnips/buffer_helper.py +++ b/pythonx/UltiSnips/buffer_helper.py @@ -1,43 +1,80 @@ # coding=utf8 import vim +import UltiSnips._vim +from UltiSnips.compatibility import as_unicode, as_vimencoding from UltiSnips.position import Position from UltiSnips._diff import diff +from UltiSnips import _vim -class VimBufferHelper: +from contextlib import contextmanager + +@contextmanager +def use_proxy_buffer(snippets_stack): + buffer_proxy = VimBufferHelper(snippets_stack) + old_buffer = _vim.buf + try: + _vim.buf = buffer_proxy + yield + finally: + _vim.buf = old_buffer + buffer_proxy.validate_buffer() + +@contextmanager +def suspend_proxy_edits(): + if not isinstance(_vim.buf, VimBufferHelper): + yield + else: + try: + _vim.buf._disable_edits() + yield + finally: + _vim.buf._enable_edits() + +class VimBufferHelper(_vim.VimBuffer): def __init__(self, snippets_stack): self._snippets_stack = snippets_stack self._buffer = vim.current.buffer self._change_tick = int(vim.eval("b:changedtick")) + self._forward_edits = True def is_buffer_changed_outside(self): - return self._change_tick != int(vim.eval("b:changedtick")) + return self._change_tick < int(vim.eval("b:changedtick")) 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') + 'vim.current.buffer; that changes are untrackable and leads to ' + + 'errors in snippet expansion; use special variable `snip.buffer` ' + 'for buffer modifications') def __setitem__(self, key, value): if isinstance(key, slice): + value = [as_vimencoding(l) for l in value] changes = list(self._get_diff(key.start, key.stop, value)) self._buffer[key.start:key.stop] = value else: + value = as_vimencoding(value) changes = list(self._get_line_diff(key, self._buffer[key], value)) self._buffer[key] = value self._change_tick += 1 - for change in changes: - self._apply_change(change) + if self._forward_edits: + for change in changes: + self._apply_change(change) + + def __setslice__(self, i, j, text): + self.__setitem__(slice(i, j), text) def __getitem__(self, key): if isinstance(key, slice): - return self._buffer[key.start:key.stop] + return [as_unicode(l) for l in self._buffer[key.start:key.stop]] else: - return self._buffer[key] + return as_unicode(self._buffer[key]) + + def __getslice__(self, i, j): + return self.__getitem__(slice(i, j)) def __len__(self): return len(self._buffer) @@ -47,7 +84,7 @@ class VimBufferHelper: line_number = len(self) if not isinstance(line, list): line = [line] - self[line_number:line_number] = line + self[line_number:line_number] = [as_vimencoding(l) for l in line] def __delitem__(self, key): if isinstance(key, slice): @@ -89,3 +126,9 @@ class VimBufferHelper: ) else: self._snippets_stack[0]._do_edit(change) + + def _disable_edits(self): + self._forward_edits = False + + def _enable_edits(self): + self._forward_edits = True diff --git a/pythonx/UltiSnips/snippet/definition/_base.py b/pythonx/UltiSnips/snippet/definition/_base.py index 9b2a96a..d9aa791 100644 --- a/pythonx/UltiSnips/snippet/definition/_base.py +++ b/pythonx/UltiSnips/snippet/definition/_base.py @@ -14,7 +14,6 @@ from UltiSnips.text import escape from UltiSnips.text_objects import SnippetInstance from UltiSnips.text_objects._python_code import SnippetUtilForAction, SnippetUtilCursor from UltiSnips.position import Position -from UltiSnips.buffer_helper import VimBufferHelper __WHITESPACE_SPLIT = re.compile(r"\s") def split_at_whitespace(string): @@ -130,46 +129,40 @@ class SnippetDefinition(object): additional_locals={} ): mark_to_use = '`' - mark_pos = _vim.get_mark_pos(mark_to_use) + with _vim.save_mark(mark_to_use): + _vim.set_mark_from_pos(mark_to_use, _vim.get_cursor_pos()) - _vim.set_mark_from_pos(mark_to_use, _vim.get_cursor_pos()) + cursor_line_before = _vim.buf.line_till_cursor - cursor_line_before = _vim.buf.line_till_cursor + locals = { + 'context': context, + } - locals = { - 'context': context, - } + locals.update(additional_locals) - locals.update(additional_locals) + snip = self._eval_code(action, locals) - snip = self._eval_code(action, locals) - - if snip.cursor.is_set(): - vim.current.window.cursor = snip.cursor.to_vim_cursor() - else: - new_mark_pos = _vim.get_mark_pos(mark_to_use) - - cursor_invalid = False - - if _vim._is_pos_zero(new_mark_pos): - cursor_invalid = True + if snip.cursor.is_set(): + vim.current.window.cursor = snip.cursor.to_vim_cursor() else: - _vim.set_cursor_from_pos(new_mark_pos) - if cursor_line_before != _vim.buf.line_till_cursor: + new_mark_pos = _vim.get_mark_pos(mark_to_use) + + cursor_invalid = False + + if _vim._is_pos_zero(new_mark_pos): cursor_invalid = True + else: + _vim.set_cursor_from_pos(new_mark_pos) + if cursor_line_before != _vim.buf.line_till_cursor: + cursor_invalid = True - if cursor_invalid: - raise RuntimeError( - 'line under the cursor was modified, but "snip.cursor" ' + - 'variable is not set; either set set "snip.cursor" to ' + - 'new cursor position, or do not modify cursor line' - ) - - # 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) + if cursor_invalid: + raise RuntimeError( + 'line under the cursor was modified, but ' + + '"snip.cursor" variable is not set; either set set ' + + '"snip.cursor" to new cursor position, or do not ' + + 'modify cursor line' + ) return snip @@ -309,9 +302,8 @@ class SnippetDefinition(object): 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} + locals = {'buffer': _vim.buf, 'visual_content': visual_content} snip = self._execute_action( self._actions['pre_expand'], self._context, locals @@ -319,55 +311,53 @@ class SnippetDefinition(object): self._context = snip.context - return buffer, snip.cursor.is_set() + return snip.cursor.is_set() else: - return buffer, False + return 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 + 'buffer': _vim.buf } snip = self._execute_action( - self._actions['post_expand'], snippets_stack[0].context, locals + self._actions['post_expand'], snippets_stack[-1].context, locals ) - snippets_stack[0].context = snip.context + snippets_stack[-1].context = snip.context - return buffer, snip.cursor.is_set() + return snip.cursor.is_set() else: - return buffer, False + return 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 + start = snippets_stack[-1].start + end = snippets_stack[-1].end locals = { 'tabstop': tabstop_number, 'jump_direction': jump_direction, - 'tabstops': snippets_stack[0].get_tabstops(), + 'tabstops': snippets_stack[-1].get_tabstops(), 'snippet_start': start, 'snippet_end': end, - 'buffer': buffer + 'buffer': _vim.buf } snip = self._execute_action( - self._actions['post_jump'], snippets_stack[0].context, locals + self._actions['post_jump'], snippets_stack[-1].context, locals ) - snippets_stack[0].context = snip.context + snippets_stack[-1].context = snip.context - return buffer, snip.cursor.is_set() + return snip.cursor.is_set() else: - return buffer, (False, None) + return False def launch(self, text_before, visual_content, parent, start, end): diff --git a/pythonx/UltiSnips/snippet_manager.py b/pythonx/UltiSnips/snippet_manager.py index 7112f44..f2a8d8a 100644 --- a/pythonx/UltiSnips/snippet_manager.py +++ b/pythonx/UltiSnips/snippet_manager.py @@ -18,6 +18,7 @@ from UltiSnips.snippet.source import UltiSnipsFileSource, SnipMateFileSource, \ find_all_snippet_files, find_snippet_files, AddedSnippetsSource from UltiSnips.text import escape from UltiSnips.vim_state import VimState, VisualContentPreserver +from UltiSnips.buffer_helper import use_proxy_buffer, suspend_proxy_edits def _ask_user(a, formatted): @@ -434,59 +435,61 @@ class SnippetManager(object): self._teardown_inner_state() 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') + with _vim.toggle_opt('ve', 'onemore'): + """Helper method that does the actual jump.""" + jumped = False - # If next tab has length 1 and the distance between itself and - # self._ctab is 1 then there is 1 less CursorMove events. We - # cannot ignore next movement in such case. - ntab_short_and_near = False - if self._cs: - ntab = self._cs.select_next_tab(backwards) - if ntab: - if self._cs.snippet.has_option('s'): - lineno = _vim.buf.cursor.line - _vim.buf[lineno] = _vim.buf[lineno].rstrip() - _vim.select(ntab.start, ntab.end) - jumped = True - if (self._ctab is not None - and ntab.start - self._ctab.end == Position(0, 1) - and ntab.end - ntab.start == Position(0, 1)): - ntab_short_and_near = True - if ntab.number == 0: + # We need to remember current snippets stack here because of + # post-jump action on the last tabstop should be able to access + # snippet instance which is ended just now. + stack_for_post_jump = self._csnippets[:] + + # If next tab has length 1 and the distance between itself and + # self._ctab is 1 then there is 1 less CursorMove events. We + # cannot ignore next movement in such case. + ntab_short_and_near = False + if self._cs: + ntab = self._cs.select_next_tab(backwards) + if ntab: + if self._cs.snippet.has_option('s'): + lineno = _vim.buf.cursor.line + _vim.buf[lineno] = _vim.buf[lineno].rstrip() + _vim.select(ntab.start, ntab.end) + jumped = True + if (self._ctab is not None + and ntab.start - self._ctab.end == Position(0, 1) + and ntab.end - ntab.start == Position(0, 1)): + ntab_short_and_near = True + if ntab.number == 0: + self._current_snippet_is_done() + self._ctab = ntab + else: + # This really shouldn't happen, because a snippet should + # have been popped when its final tabstop was used. + # Cleanup by removing current snippet and recursing. self._current_snippet_is_done() - self._ctab = ntab - else: - # This really shouldn't happen, because a snippet should - # have been popped when its final tabstop was used. - # Cleanup by removing current snippet and recursing. - self._current_snippet_is_done() - jumped = self._jump(backwards) - if jumped: - self._vstate.remember_position() - self._vstate.remember_unnamed_register(self._ctab.current_text) - if not ntab_short_and_near: - self._ignore_movements = True + jumped = self._jump(backwards) + if jumped: + self._vstate.remember_position() + self._vstate.remember_unnamed_register(self._ctab.current_text) + if not ntab_short_and_near: + self._ignore_movements = True - if len(stack_for_post_jump) > 0 and ntab is not None: - stack_for_post_jump[0].snippet.do_post_jump( - ntab.number, - -1 if backwards else 1, - stack_for_post_jump - ) + if len(stack_for_post_jump) > 0 and ntab is not None: + if self._cs: + snippet_for_action = self._cs + else: + snippet_for_action = stack_for_post_jump[-1] - _vim.command('set ve=' + old_virtualedit) + with use_proxy_buffer(stack_for_post_jump): + snippet_for_action.snippet.do_post_jump( + ntab.number, + -1 if backwards else 1, + stack_for_post_jump + ) return jumped @@ -588,54 +591,59 @@ 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() + with use_proxy_buffer(self._csnippets): + cursor_set_in_action = snippet.do_pre_expand( + self._visual_content.text, + self._csnippets + ) 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)) + with suspend_proxy_edits(): + if self._cs: + start = Position(_vim.buf.cursor.line, len(text_before)) + end = Position(_vim.buf.cursor.line, len(before)) - # It could be that our trigger contains the content of TextObjects - # in our containing snippet. If this is indeed the case, we have to - # make sure that those are properly killed. We do this by - # pretending that the user deleted and retyped the text that our - # trigger matched. - edit_actions = [ - ('D', start.line, start.col, snippet.matched), - ('I', start.line, start.col, snippet.matched), - ] - self._csnippets[0].replay_user_edits(edit_actions) + # If cursor is set in pre-action, then action was modified + # cursor line, in that case we do not need to do any edits, it + # can break snippet + if not cursor_set_in_action: + # It could be that our trigger contains the content of + # TextObjects in our containing snippet. If this is indeed + # the case, we have to make sure that those are properly + # killed. We do this by pretending that the user deleted + # and retyped the text that our trigger matched. + edit_actions = [ + ('D', start.line, start.col, snippet.matched), + ('I', start.line, start.col, snippet.matched), + ] + self._csnippets[0].replay_user_edits(edit_actions) - si = snippet.launch(text_before, self._visual_content, - self._cs.find_parent_for_new_to(start), start, end) - else: - start = Position(_vim.buf.cursor.line, len(text_before)) - end = Position(_vim.buf.cursor.line, len(before)) - si = snippet.launch(text_before, self._visual_content, - None, start, end) + si = snippet.launch(text_before, self._visual_content, + self._cs.find_parent_for_new_to(start), + start, end + ) + else: + start = Position(_vim.buf.cursor.line, len(text_before)) + end = Position(_vim.buf.cursor.line, len(before)) + si = snippet.launch(text_before, self._visual_content, + None, start, end) - self._visual_content.reset() - self._csnippets.append(si) + self._visual_content.reset() + self._csnippets.append(si) - si.update_textobjects() + si.update_textobjects() - new_buffer, _ = snippet.do_post_expand( - si._start, si._end, self._csnippets - ) + with use_proxy_buffer(self._csnippets): + snippet.do_post_expand( + si._start, si._end, self._csnippets + ) - new_buffer.validate_buffer() + self._vstate.remember_buffer(self._csnippets[0]) - self._vstate.remember_buffer(self._csnippets[0]) - - self._jump() + self._jump() def _try_expand(self): """Try to expand a snippet in the current place.""" diff --git a/pythonx/UltiSnips/text_objects/_base.py b/pythonx/UltiSnips/text_objects/_base.py index db18e59..645602b 100644 --- a/pythonx/UltiSnips/text_objects/_base.py +++ b/pythonx/UltiSnips/text_objects/_base.py @@ -178,6 +178,8 @@ class EditableTextObject(TextObject): for children in self._editable_children: if children._start <= pos < children._end: return children.find_parent_for_new_to(pos) + if children._start == pos == children._end: + return children.find_parent_for_new_to(pos) return self ############################### @@ -222,7 +224,8 @@ class EditableTextObject(TextObject): else: child._do_edit(cmd, ctab) return - elif ((pos < child._start and child._end <= delend) or + elif ((pos < child._start and child._end <= delend and + child.start < delend) or (pos <= child._start and child._end < delend)): # Case: this deletion removes the child to_kill.add(child) diff --git a/pythonx/UltiSnips/text_objects/_python_code.py b/pythonx/UltiSnips/text_objects/_python_code.py index db3c060..5faadff 100644 --- a/pythonx/UltiSnips/text_objects/_python_code.py +++ b/pythonx/UltiSnips/text_objects/_python_code.py @@ -10,6 +10,7 @@ from UltiSnips import _vim from UltiSnips.compatibility import as_unicode from UltiSnips.indent_util import IndentUtil from UltiSnips.text_objects._base import NoneditableTextObject +import UltiSnips class _Tabs(object): @@ -35,6 +36,10 @@ class SnippetUtilForAction(dict): super(SnippetUtilForAction, self).__init__(*args, **kwargs) self.__dict__ = self + def expand_anon(self, snippet): + UltiSnips.UltiSnips_Manager.expand_anon(snippet) + self.cursor.preserve() + class SnippetUtilCursor(object): def __init__(self, cursor): @@ -80,11 +85,12 @@ class SnippetUtil(object): """ - def __init__(self, initial_indent, vmode, vtext): + def __init__(self, initial_indent, vmode, vtext, context): self._ind = IndentUtil() self._visual = _VisualContent(vmode, vtext) self._initial_indent = self._ind.indent_to_spaces(initial_indent) self._reset('') + self._context = context def _reset(self, cur): """Gets the snippet ready for another update. @@ -191,6 +197,10 @@ class SnippetUtil(object): """Content of visual expansions.""" return self._visual + @property + def context(self): + return self._context + def opt(self, option, default=None): # pylint:disable=no-self-use """Gets a Vim variable.""" if _vim.eval("exists('%s')" % option) == '1': @@ -228,10 +238,11 @@ class PythonCode(NoneditableTextObject): self._locals = snippet.locals text = snippet.visual_content.text mode = snippet.visual_content.mode + context = snippet.context break except AttributeError: snippet = snippet._parent # pylint:disable=protected-access - self._snip = SnippetUtil(token.indent, mode, text) + self._snip = SnippetUtil(token.indent, mode, text, context) self._codes = (( 'import re, os, vim, string, random', diff --git a/pythonx/UltiSnips/text_objects/_tabstop.py b/pythonx/UltiSnips/text_objects/_tabstop.py index 9016cb3..f113f75 100644 --- a/pythonx/UltiSnips/text_objects/_tabstop.py +++ b/pythonx/UltiSnips/text_objects/_tabstop.py @@ -37,5 +37,9 @@ class TabStop(EditableTextObject): return self._parent is None def __repr__(self): + try: + text = self.current_text + except IndexError: + text = '' return 'TabStop(%s,%r->%r,%r)' % (self.number, self._start, - self._end, self.current_text) + self._end, text) diff --git a/test/test_ContextSnippets.py b/test/test_ContextSnippets.py index 307438f..69d07b9 100644 --- a/test/test_ContextSnippets.py +++ b/test/test_ContextSnippets.py @@ -132,7 +132,7 @@ class ContextSnippets_ReportErrorOnIndexOutOfRange(_VimTest): class ContextSnippets_CursorIsZeroBased(_VimTest): files = { 'us/all.snippets': r""" snippet e "desc" "snip.cursor" e - `!p snip.rv = str(context)` + `!p snip.rv = str(snip.context)` endsnippet """} diff --git a/test/test_SnippetActions.py b/test/test_SnippetActions.py index d29a719..9c6fdd8 100644 --- a/test/test_SnippetActions.py +++ b/test/test_SnippetActions.py @@ -229,9 +229,7 @@ class SnippetActions_CanExpandAnonSnippetInJumpAction(_VimTest): global !p def expand_anon(snip): if snip.tabstop == 0: - from UltiSnips import UltiSnips_Manager - UltiSnips_Manager.expand_anon("a($2, $1)") - snip.cursor.preserve() + snip.expand_anon("a($2, $1)") endglobal post_jump "expand_anon(snip)" @@ -250,9 +248,7 @@ class SnippetActions_CanExpandAnonSnippetInJumpActionWhileSelected(_VimTest): global !p def expand_anon(snip): if snip.tabstop == 0: - from UltiSnips import UltiSnips_Manager - UltiSnips_Manager.expand_anon(" // a($2, $1)") - snip.cursor.preserve() + snip.expand_anon(" // a($2, $1)") endglobal post_jump "expand_anon(snip)" @@ -276,3 +272,38 @@ class SnippetActions_CanUseContextFromContextMatch(_VimTest): keys = "i" + EX wanted = """some context body""" + +class SnippetActions_CanExpandAnonSnippetOnFirstJump(_VimTest): + files = { 'us/all.snippets': r""" + global !p + def expand_new_snippet_on_first_jump(snip): + if snip.tabstop == 1: + snip.expand_anon("some_check($1, $2, $3)") + endglobal + + post_jump "expand_new_snippet_on_first_jump(snip)" + snippet "test" "test new features" "True" bwre + if $1: $2 + endsnippet + """} + keys = "test" + EX + "1" + JF + "2" + JF + "3" + JF + " or 4" + JF + "5" + wanted = """if some_check(1, 2, 3) or 4: 5""" + +class SnippetActions_CanExpandAnonOnPreExpand(_VimTest): + files = { 'us/all.snippets': r""" + pre_expand "snip.buffer[snip.line] = ''; snip.expand_anon('totally_different($2, $1)')" + snippet test "test new features" wb + endsnippet + """} + keys = "test" + EX + "1" + JF + "2" + JF + "3" + wanted = """totally_different(2, 1)3""" + +class SnippetActions_CanEvenWrapSnippetInPreAction(_VimTest): + files = { 'us/all.snippets': r""" + pre_expand "snip.buffer[snip.line] = ''; snip.expand_anon('some_wrapper($1): $2')" + snippet test "test new features" wb + wrapme($2, $1) + endsnippet + """} + keys = "test" + EX + "1" + JF + "2" + JF + "3" + JF + "4" + wanted = """some_wrapper(wrapme(2, 1)3): 4""" diff --git a/test/test_TabStop.py b/test/test_TabStop.py index 1f7e2c7..c9e7648 100644 --- a/test/test_TabStop.py +++ b/test/test_TabStop.py @@ -380,3 +380,29 @@ class TabStop_AdjacentTabStopAddText_ExpectCorrectResult(_VimTest): snippets = ('test', '[ $1$2 ] $1') keys = 'test' + EX + 'Hello' + JF + 'World' + JF wanted = '[ HelloWorld ] Hello' + + +class TabStop_KeepCorrectJumpListOnOverwriteOfPartOfSnippet(_VimTest): + files = { 'us/all.snippets': r""" + snippet i + ia$1: $2 + endsnippet + + snippet ia + ia($1, $2) + endsnippet"""} + keys = 'i' + EX + EX + '1' + JF + '2' + JF + ' after' + JF + '3' + wanted = 'ia(1, 2) after: 3' + + +class TabStop_KeepCorrectJumpListOnOverwriteOfPartOfSnippetRE(_VimTest): + files = { 'us/all.snippets': r""" + snippet i + ia$1: $2 + endsnippet + + snippet "^ia" "regexp" r + ia($1, $2) + endsnippet"""} + keys = 'i' + EX + EX + '1' + JF + '2' + JF + ' after' + JF + '3' + wanted = 'ia(1, 2) after: 3' From bc29e23226144b9fb98bddb89902541c4727a2aa Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Mon, 15 Jun 2015 13:58:12 +0600 Subject: [PATCH 06/19] VimBufferHelper -> VimBufferProxy --- pythonx/UltiSnips/buffer_helper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pythonx/UltiSnips/buffer_helper.py b/pythonx/UltiSnips/buffer_helper.py index fc55da6..40366d1 100644 --- a/pythonx/UltiSnips/buffer_helper.py +++ b/pythonx/UltiSnips/buffer_helper.py @@ -11,7 +11,7 @@ from contextlib import contextmanager @contextmanager def use_proxy_buffer(snippets_stack): - buffer_proxy = VimBufferHelper(snippets_stack) + buffer_proxy = VimBufferProxy(snippets_stack) old_buffer = _vim.buf try: _vim.buf = buffer_proxy @@ -22,7 +22,7 @@ def use_proxy_buffer(snippets_stack): @contextmanager def suspend_proxy_edits(): - if not isinstance(_vim.buf, VimBufferHelper): + if not isinstance(_vim.buf, VimBufferProxy): yield else: try: @@ -31,7 +31,7 @@ def suspend_proxy_edits(): finally: _vim.buf._enable_edits() -class VimBufferHelper(_vim.VimBuffer): +class VimBufferProxy(_vim.VimBuffer): def __init__(self, snippets_stack): self._snippets_stack = snippets_stack self._buffer = vim.current.buffer From 8958b71341846ae18699a7ee07b6cde9b41adc70 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Tue, 16 Jun 2015 10:19:08 +0600 Subject: [PATCH 07/19] add docs to the buffer proxy object --- .../{buffer_helper.py => buffer_proxy.py} | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) rename pythonx/UltiSnips/{buffer_helper.py => buffer_proxy.py} (64%) diff --git a/pythonx/UltiSnips/buffer_helper.py b/pythonx/UltiSnips/buffer_proxy.py similarity index 64% rename from pythonx/UltiSnips/buffer_helper.py rename to pythonx/UltiSnips/buffer_proxy.py index 40366d1..312f258 100644 --- a/pythonx/UltiSnips/buffer_helper.py +++ b/pythonx/UltiSnips/buffer_proxy.py @@ -9,8 +9,13 @@ from UltiSnips import _vim from contextlib import contextmanager + @contextmanager def use_proxy_buffer(snippets_stack): + """ + Forward all changes made in the buffer to the current snippet stack while + function call. + """ buffer_proxy = VimBufferProxy(snippets_stack) old_buffer = _vim.buf try: @@ -20,8 +25,12 @@ def use_proxy_buffer(snippets_stack): _vim.buf = old_buffer buffer_proxy.validate_buffer() + @contextmanager def suspend_proxy_edits(): + """ + Prevents changes being applied to the snippet stack while function call. + """ if not isinstance(_vim.buf, VimBufferProxy): yield else: @@ -31,17 +40,47 @@ def suspend_proxy_edits(): finally: _vim.buf._enable_edits() + class VimBufferProxy(_vim.VimBuffer): + """ + Proxy object used for tracking changes that made from snippet actions. + + Unfortunately, vim by itself lacks of the API for changing text in + trackable maner. + + Vim marks offers limited functionality for tracking line additions and + deletions, but nothing offered for tracking changes withing single line. + + Instance of this class is passed to all snippet actions and behaves as + internal vim.current.window.buffer. + + All changes that are made by user passed to diff algorithm, and resulting + diff applied to internal snippet structures to ensure they are in sync with + actual buffer contents. + """ + def __init__(self, snippets_stack): + """ + Instantiate new object. + + snippets_stack is a slice of currently active snippets. + """ self._snippets_stack = snippets_stack self._buffer = vim.current.buffer self._change_tick = int(vim.eval("b:changedtick")) self._forward_edits = True def is_buffer_changed_outside(self): + """ + Returns true, if buffer was changed without using proxy object, like + with vim.command() or through internal vim.current.window.buffer. + """ return self._change_tick < int(vim.eval("b:changedtick")) def validate_buffer(self): + """ + Raises exception if buffer is changes beyound proxy object. + """ if self.is_buffer_changed_outside(): raise RuntimeError('buffer was modified using vim.command or ' + 'vim.current.buffer; that changes are untrackable and leads to ' + @@ -49,6 +88,10 @@ class VimBufferProxy(_vim.VimBuffer): 'for buffer modifications') def __setitem__(self, key, value): + """ + Behaves as vim.current.window.buffer.__setitem__ except it tracks + changes and applies them to the current snippet stack. + """ if isinstance(key, slice): value = [as_vimencoding(l) for l in value] changes = list(self._get_diff(key.start, key.stop, value)) @@ -65,21 +108,36 @@ class VimBufferProxy(_vim.VimBuffer): self._apply_change(change) def __setslice__(self, i, j, text): + """ + Same as __setitem__. + """ self.__setitem__(slice(i, j), text) def __getitem__(self, key): + """ + Just passing call to the vim.current.window.buffer.__getitem__. + """ if isinstance(key, slice): return [as_unicode(l) for l in self._buffer[key.start:key.stop]] else: return as_unicode(self._buffer[key]) def __getslice__(self, i, j): + """ + Same as __getitem__. + """ return self.__getitem__(slice(i, j)) def __len__(self): + """ + Same as len(vim.current.window.buffer). + """ return len(self._buffer) def append(self, line, line_number=-1): + """ + Same as vim.current.window.buffer.append(), but with tracking changes. + """ if line_number < 0: line_number = len(self) if not isinstance(line, list): @@ -93,6 +151,9 @@ class VimBufferProxy(_vim.VimBuffer): self.__setitem__(slice(key, key+1), []) def _get_diff(self, start, end, new_value): + """ + Very fast diffing algorithm when changes are across many lines. + """ for line_number in range(start, end): yield ('D', line_number, 0, self._buffer[line_number]) @@ -100,6 +161,9 @@ class VimBufferProxy(_vim.VimBuffer): yield ('I', start+line_number, 0, new_value[line_number]) def _get_line_diff(self, line_number, before, after): + """ + Use precise diffing for tracking changes in single line. + """ if before == '': for change in self._get_diff(line_number, line_number+1, [after]): yield change @@ -108,6 +172,10 @@ class VimBufferProxy(_vim.VimBuffer): yield (change[0], line_number, change[2], change[3]) def _apply_change(self, change): + """ + Apply changeset to current snippets stack, correctly moving around + snippet itself or its child. + """ if not self._snippets_stack: return @@ -128,7 +196,15 @@ class VimBufferProxy(_vim.VimBuffer): self._snippets_stack[0]._do_edit(change) def _disable_edits(self): + """ + Temporary disable applying changes to snippets stack. Should be done + while expanding anonymous snippet in the middle of jump to prevent + double tracking. + """ self._forward_edits = False def _enable_edits(self): + """ + Enables changes forwarding back. + """ self._forward_edits = True From ba774c39ca953d5dabfffcd70bc710eda6ac768e Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Tue, 16 Jun 2015 11:21:38 +0600 Subject: [PATCH 08/19] review fixes --- doc/UltiSnips.txt | 4 ++++ pythonx/UltiSnips/buffer_proxy.py | 3 ++- test/vim_test_case.py | 1 - 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/UltiSnips.txt b/doc/UltiSnips.txt index dee9199..b4a58ee 100644 --- a/doc/UltiSnips.txt +++ b/doc/UltiSnips.txt @@ -1443,11 +1443,15 @@ Specified code will be evaluated at stages defined above and same global variables and modules will be available that are stated in the *UltiSnips-context-snippets* section. + *UltiSnips-buffer-proxy* + Note: special variable called 'snip.buffer' should be used for all buffer modifications. Not 'vim.current.buffer' and not 'vim.command("...")', because of in that case UltiSnips will not be able to track changes buffer from actions. +'snip.buffer' has the same interface as 'vim.current.window.buffer'. + 4.10.1 Pre-expand actions *UltiSnips-pre-expand-actions* Pre-expand actions can be used to match snippet in one location and then diff --git a/pythonx/UltiSnips/buffer_proxy.py b/pythonx/UltiSnips/buffer_proxy.py index 312f258..b0db32d 100644 --- a/pythonx/UltiSnips/buffer_proxy.py +++ b/pythonx/UltiSnips/buffer_proxy.py @@ -85,7 +85,8 @@ class VimBufferProxy(_vim.VimBuffer): raise RuntimeError('buffer was modified using vim.command or ' + 'vim.current.buffer; that changes are untrackable and leads to ' + 'errors in snippet expansion; use special variable `snip.buffer` ' - 'for buffer modifications') + 'for buffer modifications.\n\n' + + 'See :help UltiSnips-buffer-proxy for more info.') def __setitem__(self, key, value): """ diff --git a/test/vim_test_case.py b/test/vim_test_case.py index 6caa78b..f3a6e08 100644 --- a/test/vim_test_case.py +++ b/test/vim_test_case.py @@ -128,7 +128,6 @@ 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=""') From 1e771de6032881eb9fcc67a1bdd232514d371326 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Tue, 16 Jun 2015 15:56:18 +0600 Subject: [PATCH 09/19] fix tests --- pythonx/UltiSnips/snippet_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonx/UltiSnips/snippet_manager.py b/pythonx/UltiSnips/snippet_manager.py index f2a8d8a..4db00a8 100644 --- a/pythonx/UltiSnips/snippet_manager.py +++ b/pythonx/UltiSnips/snippet_manager.py @@ -18,7 +18,7 @@ from UltiSnips.snippet.source import UltiSnipsFileSource, SnipMateFileSource, \ find_all_snippet_files, find_snippet_files, AddedSnippetsSource from UltiSnips.text import escape from UltiSnips.vim_state import VimState, VisualContentPreserver -from UltiSnips.buffer_helper import use_proxy_buffer, suspend_proxy_edits +from UltiSnips.buffer_proxy import use_proxy_buffer, suspend_proxy_edits def _ask_user(a, formatted): From 5a663116f51082e605a1438503b98bf179c311d3 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Wed, 17 Jun 2015 22:12:49 +0600 Subject: [PATCH 10/19] edge case: anon snip in pre-action --- pythonx/UltiSnips/snippet_manager.py | 42 ++++++++++++++++++++++------ test/test_SnippetActions.py | 9 ++++++ 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/pythonx/UltiSnips/snippet_manager.py b/pythonx/UltiSnips/snippet_manager.py index 4db00a8..e524ce1 100644 --- a/pythonx/UltiSnips/snippet_manager.py +++ b/pythonx/UltiSnips/snippet_manager.py @@ -8,6 +8,7 @@ from functools import wraps import os import platform import traceback +from contextlib import contextmanager from UltiSnips import _vim from UltiSnips._diff import diff, guess_edit @@ -95,6 +96,9 @@ class SnippetManager(object): self._snippet_sources = [] + self._snip_expanded_in_action = False + self._inside_action = False + self._added_snippets_source = AddedSnippetsSource() self.register_snippet_source('ultisnips_files', UltiSnipsFileSource()) self.register_snippet_source('added', self._added_snippets_source) @@ -586,16 +590,19 @@ class SnippetManager(object): done with it.""" self._setup_inner_state() + self._snip_expanded_in_action = False + # Adjust before, maybe the trigger is not the complete word text_before = before if snippet.matched: text_before = before[:-len(snippet.matched)] with use_proxy_buffer(self._csnippets): - cursor_set_in_action = snippet.do_pre_expand( - self._visual_content.text, - self._csnippets - ) + with self._action_context(): + cursor_set_in_action = snippet.do_pre_expand( + self._visual_content.text, + self._csnippets + ) if cursor_set_in_action: text_before = _vim.buf.line_till_cursor @@ -637,13 +644,23 @@ class SnippetManager(object): si.update_textobjects() with use_proxy_buffer(self._csnippets): - snippet.do_post_expand( - si._start, si._end, self._csnippets - ) + with self._action_context(): + snippet.do_post_expand( + si._start, si._end, self._csnippets + ) self._vstate.remember_buffer(self._csnippets[0]) - self._jump() + if not self._snip_expanded_in_action: + self._jump() + elif self._cs.current_text != '': + self._jump() + else: + self._current_snippet_is_done() + + if self._inside_action: + self._snip_expanded_in_action = True + def _try_expand(self): """Try to expand a snippet in the current place.""" @@ -733,3 +750,12 @@ class SnippetManager(object): if not os.path.exists(dirname): os.makedirs(dirname) return file_to_edit + + @contextmanager + def _action_context(self): + try: + old_flag = self._inside_action + self._inside_action = True + yield + finally: + self._inside_action = old_flag diff --git a/test/test_SnippetActions.py b/test/test_SnippetActions.py index 9c6fdd8..6abe332 100644 --- a/test/test_SnippetActions.py +++ b/test/test_SnippetActions.py @@ -307,3 +307,12 @@ class SnippetActions_CanEvenWrapSnippetInPreAction(_VimTest): """} keys = "test" + EX + "1" + JF + "2" + JF + "3" + JF + "4" wanted = """some_wrapper(wrapme(2, 1)3): 4""" + +class SnippetActions_CanVisuallySelectFirstPlaceholderInAnonSnippetInPre(_VimTest): + files = { 'us/all.snippets': r""" + pre_expand "snip.buffer[snip.line] = ''; snip.expand_anon('${1:asd}, ${2:blah}')" + snippet test "test new features" wb + endsnippet + """} + keys = "test" + EX + "1" + JF + "2" + wanted = """1, 2""" From 9649f15a9e00294bc6eb7676095a2ec19793c603 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Fri, 19 Jun 2015 00:04:50 +0600 Subject: [PATCH 11/19] fix buffer edits from jump action --- pythonx/UltiSnips/buffer_proxy.py | 13 ++++++-- pythonx/UltiSnips/snippet/definition/_base.py | 12 ++++---- pythonx/UltiSnips/snippet_manager.py | 22 ++++++++------ test/test_SnippetActions.py | 30 +++++++++++++++++++ 4 files changed, 59 insertions(+), 18 deletions(-) diff --git a/pythonx/UltiSnips/buffer_proxy.py b/pythonx/UltiSnips/buffer_proxy.py index b0db32d..4692703 100644 --- a/pythonx/UltiSnips/buffer_proxy.py +++ b/pythonx/UltiSnips/buffer_proxy.py @@ -11,12 +11,12 @@ from contextlib import contextmanager @contextmanager -def use_proxy_buffer(snippets_stack): +def use_proxy_buffer(snippets_stack, vstate): """ Forward all changes made in the buffer to the current snippet stack while function call. """ - buffer_proxy = VimBufferProxy(snippets_stack) + buffer_proxy = VimBufferProxy(snippets_stack, vstate) old_buffer = _vim.buf try: _vim.buf = buffer_proxy @@ -59,7 +59,7 @@ class VimBufferProxy(_vim.VimBuffer): actual buffer contents. """ - def __init__(self, snippets_stack): + def __init__(self, snippets_stack, vstate): """ Instantiate new object. @@ -69,6 +69,7 @@ class VimBufferProxy(_vim.VimBuffer): self._buffer = vim.current.buffer self._change_tick = int(vim.eval("b:changedtick")) self._forward_edits = True + self._vstate = vstate def is_buffer_changed_outside(self): """ @@ -107,6 +108,8 @@ class VimBufferProxy(_vim.VimBuffer): if self._forward_edits: for change in changes: self._apply_change(change) + if self._snippets_stack: + self._vstate.remember_buffer(self._snippets_stack[0]) def __setslice__(self, i, j, text): """ @@ -194,6 +197,10 @@ class VimBufferProxy(_vim.VimBuffer): Position(direction, 0) ) else: + if line_number > self._snippets_stack[0]._end.line: + return + if column_number > self._snippets_stack[0]._end.col: + return self._snippets_stack[0]._do_edit(change) def _disable_edits(self): diff --git a/pythonx/UltiSnips/snippet/definition/_base.py b/pythonx/UltiSnips/snippet/definition/_base.py index d9aa791..5757bb0 100644 --- a/pythonx/UltiSnips/snippet/definition/_base.py +++ b/pythonx/UltiSnips/snippet/definition/_base.py @@ -334,26 +334,26 @@ class SnippetDefinition(object): return False def do_post_jump( - self, tabstop_number, jump_direction, snippets_stack + self, tabstop_number, jump_direction, snippets_stack, current_snippet ): if 'post_jump' in self._actions: - start = snippets_stack[-1].start - end = snippets_stack[-1].end + start = current_snippet.start + end = current_snippet.end locals = { 'tabstop': tabstop_number, 'jump_direction': jump_direction, - 'tabstops': snippets_stack[-1].get_tabstops(), + 'tabstops': current_snippet.get_tabstops(), 'snippet_start': start, 'snippet_end': end, 'buffer': _vim.buf } snip = self._execute_action( - self._actions['post_jump'], snippets_stack[-1].context, locals + self._actions['post_jump'], current_snippet.context, locals ) - snippets_stack[-1].context = snip.context + current_snippet.context = snip.context return snip.cursor.is_set() else: diff --git a/pythonx/UltiSnips/snippet_manager.py b/pythonx/UltiSnips/snippet_manager.py index e524ce1..87ebc54 100644 --- a/pythonx/UltiSnips/snippet_manager.py +++ b/pythonx/UltiSnips/snippet_manager.py @@ -455,6 +455,14 @@ class SnippetManager(object): # self._ctab is 1 then there is 1 less CursorMove events. We # cannot ignore next movement in such case. ntab_short_and_near = False + + if self._cs: + snippet_for_action = self._cs + elif stack_for_post_jump: + snippet_for_action = stack_for_post_jump[-1] + else: + snippet_for_action = None + if self._cs: ntab = self._cs.select_next_tab(backwards) if ntab: @@ -483,16 +491,12 @@ class SnippetManager(object): self._ignore_movements = True if len(stack_for_post_jump) > 0 and ntab is not None: - if self._cs: - snippet_for_action = self._cs - else: - snippet_for_action = stack_for_post_jump[-1] - - with use_proxy_buffer(stack_for_post_jump): + with use_proxy_buffer(stack_for_post_jump, self._vstate): snippet_for_action.snippet.do_post_jump( ntab.number, -1 if backwards else 1, - stack_for_post_jump + stack_for_post_jump, + snippet_for_action ) return jumped @@ -597,7 +601,7 @@ class SnippetManager(object): if snippet.matched: text_before = before[:-len(snippet.matched)] - with use_proxy_buffer(self._csnippets): + with use_proxy_buffer(self._csnippets, self._vstate): with self._action_context(): cursor_set_in_action = snippet.do_pre_expand( self._visual_content.text, @@ -643,7 +647,7 @@ class SnippetManager(object): si.update_textobjects() - with use_proxy_buffer(self._csnippets): + with use_proxy_buffer(self._csnippets, self._vstate): with self._action_context(): snippet.do_post_expand( si._start, si._end, self._csnippets diff --git a/test/test_SnippetActions.py b/test/test_SnippetActions.py index 6abe332..95a9216 100644 --- a/test/test_SnippetActions.py +++ b/test/test_SnippetActions.py @@ -316,3 +316,33 @@ class SnippetActions_CanVisuallySelectFirstPlaceholderInAnonSnippetInPre(_VimTes """} keys = "test" + EX + "1" + JF + "2" wanted = """1, 2""" + +class SnippetActions_UseCorrectJumpActions(_VimTest): + files = { 'us/all.snippets': r""" + post_jump "snip.buffer[-2:-2]=['a' + str(snip.tabstop)]" + snippet a "a" wb + $1 { + $2 + } + endsnippet + + snippet b "b" wb + bbb + endsnippet + + post_jump "snip.buffer[-2:-2]=['c' + str(snip.tabstop)]" + snippet c "c" w + $1 : $2 : $3 + endsnippet + """} + keys = "a" + EX + "1" + JF + "b" + EX + " c" + EX + "2" + JF + "3" + JF + "4" + JF + JF + wanted = """1 { +bbb 2 : 3 : 4 +} +a1 +a2 +c1 +c2 +c3 +c0 +a0""" From b50c5c86c3de7f1a4b313011a08a7a4b57e54c38 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Fri, 19 Jun 2015 11:44:29 +0600 Subject: [PATCH 12/19] pass all args to snip.expand_anon --- pythonx/UltiSnips/text_objects/_python_code.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pythonx/UltiSnips/text_objects/_python_code.py b/pythonx/UltiSnips/text_objects/_python_code.py index 5faadff..2828618 100644 --- a/pythonx/UltiSnips/text_objects/_python_code.py +++ b/pythonx/UltiSnips/text_objects/_python_code.py @@ -36,8 +36,8 @@ class SnippetUtilForAction(dict): super(SnippetUtilForAction, self).__init__(*args, **kwargs) self.__dict__ = self - def expand_anon(self, snippet): - UltiSnips.UltiSnips_Manager.expand_anon(snippet) + def expand_anon(self, *args, **kwargs): + UltiSnips.UltiSnips_Manager.expand_anon(*args, **kwargs) self.cursor.preserve() From 3c4ac11af3c93346b0f799a2b97a213fe192552e Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Wed, 24 Jun 2015 15:05:48 +0600 Subject: [PATCH 13/19] fix python3, finally --- autoload/UltiSnips/bootstrap.vim | 99 +++++++++++++++++++ pythonx/UltiSnips/__init__.py | 9 -- pythonx/UltiSnips/buffer_proxy.py | 4 + pythonx/UltiSnips/snippet_manager.py | 6 ++ .../UltiSnips/text_objects/_python_code.py | 6 +- x | 0 6 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 autoload/UltiSnips/bootstrap.vim create mode 100644 x diff --git a/autoload/UltiSnips/bootstrap.vim b/autoload/UltiSnips/bootstrap.vim new file mode 100644 index 0000000..e8ea8e9 --- /dev/null +++ b/autoload/UltiSnips/bootstrap.vim @@ -0,0 +1,99 @@ +let s:SourcedFile=expand("") + +function! UltiSnips#bootstrap#Bootstrap() + if exists('s:did_UltiSnips_bootstrap') + return + endif + let s:did_UltiSnips_bootstrap=1 + + if !exists("g:UltiSnipsUsePythonVersion") + let g:_uspy=":py3 " + if !has("python3") + if !has("python") + if !exists("g:UltiSnipsNoPythonWarning") + echohl WarningMsg + echom "UltiSnips requires py >= 2.7 or any py3" + echohl None + endif + unlet g:_uspy + return + endif + let g:_uspy=":py " + endif + let g:UltiSnipsUsePythonVersion = "" + else + " Use user-provided value, but check if it's available. + " This uses `has()`, because e.g. `exists(":python3")` is always 2. + if g:UltiSnipsUsePythonVersion == 2 && has('python') + let g:_uspy=":python " + elseif g:UltiSnipsUsePythonVersion == 3 && has('python3') + let g:_uspy=":python3 " + endif + if !exists('g:_uspy') + echohl WarningMsg + echom "UltiSnips: the Python version from g:UltiSnipsUsePythonVersion (".g:UltiSnipsUsePythonVersion.") is not available." + echohl None + return + endif + endif + + " Expand our path + exec g:_uspy "import vim, os, sys" + exec g:_uspy "sourced_file = vim.eval('s:SourcedFile')" + exec g:_uspy "while not os.path.exists(os.path.join(sourced_file, 'pythonx')): + \ sourced_file = os.path.dirname(sourced_file)" + exec g:_uspy "module_path = os.path.join(sourced_file, 'pythonx')" + exec g:_uspy "vim.command(\"let g:UltiSnipsPythonPath = '%s'\" % module_path)" + exec g:_uspy "if not hasattr(vim, 'VIM_SPECIAL_PATH'): sys.path.append(module_path)" + exec g:_uspy "from UltiSnips.snippet_manager import UltiSnips_Manager" +endfunction + +" The trigger used to expand a snippet. +" NOTE: expansion and forward jumping can, but needn't be the same trigger +if !exists("g:UltiSnipsExpandTrigger") + let g:UltiSnipsExpandTrigger = "" +endif + +" The trigger used to display all triggers that could possible +" match in the current position. +if !exists("g:UltiSnipsListSnippets") + let g:UltiSnipsListSnippets = "" +endif + +" The trigger used to jump forward to the next placeholder. +" NOTE: expansion and forward jumping can, but needn't be the same trigger +if !exists("g:UltiSnipsJumpForwardTrigger") + let g:UltiSnipsJumpForwardTrigger = "" +endif + +" The trigger to jump backward inside a snippet +if !exists("g:UltiSnipsJumpBackwardTrigger") + let g:UltiSnipsJumpBackwardTrigger = "" +endif + +" Should UltiSnips unmap select mode mappings automagically? +if !exists("g:UltiSnipsRemoveSelectModeMappings") + let g:UltiSnipsRemoveSelectModeMappings = 1 +end + +" If UltiSnips should remove Mappings, which should be ignored +if !exists("g:UltiSnipsMappingsToIgnore") + let g:UltiSnipsMappingsToIgnore = [] +endif + +" UltiSnipsEdit will use this variable to decide if a new window +" is opened when editing. default is "normal", allowed are also +" "vertical", "horizontal" +if !exists("g:UltiSnipsEditSplit") + let g:UltiSnipsEditSplit = 'normal' +endif + +" A list of directory names that are searched for snippets. +if !exists("g:UltiSnipsSnippetDirectories") + let g:UltiSnipsSnippetDirectories = [ "UltiSnips" ] +endif + +" Enable or Disable snipmate snippet expansion. +if !exists("g:UltiSnipsEnableSnipMate") + let g:UltiSnipsEnableSnipMate = 1 +endif diff --git a/pythonx/UltiSnips/__init__.py b/pythonx/UltiSnips/__init__.py index 9566f08..47ecd2d 100644 --- a/pythonx/UltiSnips/__init__.py +++ b/pythonx/UltiSnips/__init__.py @@ -2,12 +2,3 @@ # encoding: utf-8 """Entry point for all thinks UltiSnips.""" - -import vim # pylint:disable=import-error - -from UltiSnips.snippet_manager import SnippetManager - -UltiSnips_Manager = SnippetManager( # pylint:disable=invalid-name - vim.eval('g:UltiSnipsExpandTrigger'), - vim.eval('g:UltiSnipsJumpForwardTrigger'), - vim.eval('g:UltiSnipsJumpBackwardTrigger')) diff --git a/pythonx/UltiSnips/buffer_proxy.py b/pythonx/UltiSnips/buffer_proxy.py index 4692703..9899874 100644 --- a/pythonx/UltiSnips/buffer_proxy.py +++ b/pythonx/UltiSnips/buffer_proxy.py @@ -159,8 +159,12 @@ class VimBufferProxy(_vim.VimBuffer): Very fast diffing algorithm when changes are across many lines. """ for line_number in range(start, end): + if line_number < 0: + line_number = len(self._buffer) + line_number yield ('D', line_number, 0, self._buffer[line_number]) + if start < 0: + start = len(self._buffer) + start for line_number in range(0, len(new_value)): yield ('I', start+line_number, 0, new_value[line_number]) diff --git a/pythonx/UltiSnips/snippet_manager.py b/pythonx/UltiSnips/snippet_manager.py index 87ebc54..f144fbf 100644 --- a/pythonx/UltiSnips/snippet_manager.py +++ b/pythonx/UltiSnips/snippet_manager.py @@ -8,6 +8,7 @@ from functools import wraps import os import platform import traceback +import vim from contextlib import contextmanager from UltiSnips import _vim @@ -763,3 +764,8 @@ class SnippetManager(object): yield finally: self._inside_action = old_flag + +UltiSnips_Manager = SnippetManager( # pylint:disable=invalid-name + vim.eval('g:UltiSnipsExpandTrigger'), + vim.eval('g:UltiSnipsJumpForwardTrigger'), + vim.eval('g:UltiSnipsJumpBackwardTrigger')) diff --git a/pythonx/UltiSnips/text_objects/_python_code.py b/pythonx/UltiSnips/text_objects/_python_code.py index 2828618..9427c01 100644 --- a/pythonx/UltiSnips/text_objects/_python_code.py +++ b/pythonx/UltiSnips/text_objects/_python_code.py @@ -10,7 +10,7 @@ from UltiSnips import _vim from UltiSnips.compatibility import as_unicode from UltiSnips.indent_util import IndentUtil from UltiSnips.text_objects._base import NoneditableTextObject -import UltiSnips +import UltiSnips.snippet_manager class _Tabs(object): @@ -37,7 +37,9 @@ class SnippetUtilForAction(dict): self.__dict__ = self def expand_anon(self, *args, **kwargs): - UltiSnips.UltiSnips_Manager.expand_anon(*args, **kwargs) + UltiSnips.snippet_manager.UltiSnips_Manager.expand_anon( + *args, **kwargs + ) self.cursor.preserve() diff --git a/x b/x new file mode 100644 index 0000000..e69de29 From bdecd9a8b5ebbc65e8929a5e14133021415c2f22 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Thu, 9 Jul 2015 17:26:56 +0600 Subject: [PATCH 14/19] fix not cleared context --- pythonx/UltiSnips/snippet/definition/_base.py | 1 + test/test_ContextSnippets.py | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/pythonx/UltiSnips/snippet/definition/_base.py b/pythonx/UltiSnips/snippet/definition/_base.py index 5757bb0..c6b0f62 100644 --- a/pythonx/UltiSnips/snippet/definition/_base.py +++ b/pythonx/UltiSnips/snippet/definition/_base.py @@ -242,6 +242,7 @@ class SnippetDefinition(object): self._matched = '' return False + self._context = None if match and self._context_code: self._context = self._context_match() if not self.context: diff --git a/test/test_ContextSnippets.py b/test/test_ContextSnippets.py index 69d07b9..f178b40 100644 --- a/test/test_ContextSnippets.py +++ b/test/test_ContextSnippets.py @@ -138,3 +138,14 @@ class ContextSnippets_CursorIsZeroBased(_VimTest): keys = "e" + EX wanted = "(2, 1)" + +class ContextSnippets_ContextIsClearedBeforeExpand(_VimTest): + files = { 'us/all.snippets': r""" + pre_expand "snip.context = 1 if snip.context is None else 2" + snippet e "desc" w + `!p snip.rv = str(snip.context)` + endsnippet + """} + + keys = "e" + EX + " " + "e" + EX + wanted = "1 1" From a39f3c241a6d9ad2f665103c6e0a8d6046b524b3 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Fri, 17 Jul 2015 19:14:22 +0600 Subject: [PATCH 15/19] revert access to UltiSnips_Manager --- pythonx/UltiSnips/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pythonx/UltiSnips/__init__.py b/pythonx/UltiSnips/__init__.py index 47ecd2d..dfd3f48 100644 --- a/pythonx/UltiSnips/__init__.py +++ b/pythonx/UltiSnips/__init__.py @@ -2,3 +2,5 @@ # encoding: utf-8 """Entry point for all thinks UltiSnips.""" + +from UltiSnips.snippet_manager import UltiSnips_Manager From 175e3ba5218752cd65a21e1ddee6399790447dcf Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Fri, 17 Jul 2015 19:30:55 +0600 Subject: [PATCH 16/19] fix doc 80 chars limit --- doc/UltiSnips.txt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/UltiSnips.txt b/doc/UltiSnips.txt index b4a58ee..910baa9 100644 --- a/doc/UltiSnips.txt +++ b/doc/UltiSnips.txt @@ -1473,8 +1473,8 @@ variable method 'snip.cursor.set(line, column)' must be called with the desired cursor position. In that case UltiSnips will not remove any matched trigger text and it should be done manually in action code. -To addition to the scope variables defined above 'snip.visual_content' will be also -declared and will contain text that was selected before snippet expansion +To addition to the scope variables defined above 'snip.visual_content' will be +also declared and will contain text that was selected before snippet expansion (similar to $VISUAL placeholder). Following snippet will be expanded at 4 spaces indentation level no matter @@ -1518,8 +1518,9 @@ Variables 'snip.snippet_start' and 'snip.snippet_end' will be defined at the action code scope and will point to positions of the start and end of expanded snippet accordingly in the form '(line, column)'. -Note: 'snip.snippet_start' and 'snip.snippet_end' will automatically adjust to the -correct positions if post-action will insert or delete lines before expansion. +Note: 'snip.snippet_start' and 'snip.snippet_end' will automatically adjust to +the correct positions if post-action will insert or delete lines before +expansion. Following snippet will expand to method definition and automatically insert additional newline after end of the snippet. It's very useful to create a From 58a2094d4951f09135f684f5c3b8c8a1ff6934a5 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Mon, 20 Jul 2015 12:49:32 +0600 Subject: [PATCH 17/19] review fixes --- pythonx/UltiSnips/snippet/definition/_base.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pythonx/UltiSnips/snippet/definition/_base.py b/pythonx/UltiSnips/snippet/definition/_base.py index c6b0f62..62af17e 100644 --- a/pythonx/UltiSnips/snippet/definition/_base.py +++ b/pythonx/UltiSnips/snippet/definition/_base.py @@ -10,10 +10,10 @@ import vim from UltiSnips import _vim from UltiSnips.compatibility import as_unicode from UltiSnips.indent_util import IndentUtil +from UltiSnips.position import Position from UltiSnips.text import escape from UltiSnips.text_objects import SnippetInstance -from UltiSnips.text_objects._python_code import SnippetUtilForAction, SnippetUtilCursor -from UltiSnips.position import Position +from UltiSnips.text_objects._python_code import SnippetUtilCursor, SnippetUtilForAction __WHITESPACE_SPLIT = re.compile(r"\s") def split_at_whitespace(string): @@ -104,8 +104,6 @@ class SnippetDefinition(object): current = vim.current - holder = {'result': False} - locals = { 'window': current.window, 'buffer': current.buffer, From c3add0bef443b9bbb436533c282335ffce2927e7 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Wed, 22 Jul 2015 10:49:45 +0600 Subject: [PATCH 18/19] fix neovim tests --- pythonx/UltiSnips/buffer_proxy.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pythonx/UltiSnips/buffer_proxy.py b/pythonx/UltiSnips/buffer_proxy.py index 9899874..1e007e7 100644 --- a/pythonx/UltiSnips/buffer_proxy.py +++ b/pythonx/UltiSnips/buffer_proxy.py @@ -95,9 +95,11 @@ class VimBufferProxy(_vim.VimBuffer): changes and applies them to the current snippet stack. """ if isinstance(key, slice): - value = [as_vimencoding(l) for l in value] + value = [as_vimencoding(line) for line in value] changes = list(self._get_diff(key.start, key.stop, value)) - self._buffer[key.start:key.stop] = value + self._buffer[key.start:key.stop] = [ + line.strip('\n') for line in value + ] else: value = as_vimencoding(value) changes = list(self._get_line_diff(key, self._buffer[key], value)) From 5638cdf4a2964d0285a876cf1b7da4e3d2cf5643 Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Wed, 22 Jul 2015 11:59:55 +0600 Subject: [PATCH 19/19] review fix for == --- pythonx/UltiSnips/text_objects/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonx/UltiSnips/text_objects/_base.py b/pythonx/UltiSnips/text_objects/_base.py index 645602b..6ee1745 100644 --- a/pythonx/UltiSnips/text_objects/_base.py +++ b/pythonx/UltiSnips/text_objects/_base.py @@ -178,7 +178,7 @@ class EditableTextObject(TextObject): for children in self._editable_children: if children._start <= pos < children._end: return children.find_parent_for_new_to(pos) - if children._start == pos == children._end: + if children._start == pos and pos == children._end: return children.find_parent_for_new_to(pos) return self