From 972305725f5796774e0ac0ed30a47dc1ef5bc1ab Mon Sep 17 00:00:00 2001 From: Stanislav Seletskiy Date: Wed, 10 Jun 2015 23:32:36 +0600 Subject: [PATCH] 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'