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/doc/UltiSnips.txt b/doc/UltiSnips.txt index 2c8e5e4..910baa9 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| @@ -1343,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 ------------------- @@ -1371,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 } @@ -1390,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: @@ -1405,18 +1413,192 @@ 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-snippet-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. + + *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 +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 +'snip.buffer', snippet expansion position will be automatically adjusted. + +If cursor line (where trigger was matched) need to be modified, then special +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 +(similar to $VISUAL placeholder). + +Following snippet will be expanded at 4 spaces indentation level no matter +where it was triggered. + +------------------- SNIP ------------------- +pre_expand "snip.buffer[snip.line] = ' '*4; snip.cursor.set(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 snip.buffer[snip.line]; snip.buffer.append(''); snip.cursor.set(len(snip.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 +'snip.buffer', snippet expansion position will be automatically adjusted. + +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. + +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 "snip.buffer[snip.snippet_end[0]+1:snip.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 +'snip.buffer', snippet expansion position will be automatically adjusted. + +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 + 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 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)` +$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 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 +def insert_method_call(name): + vim.command('normal G') + snip.expand_anon(name + '($1)\n') +endglobal + +post_jump "if snip.tabstop == 0: insert_method_call(snip.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/__init__.py b/pythonx/UltiSnips/__init__.py index 9566f08..dfd3f48 100644 --- a/pythonx/UltiSnips/__init__.py +++ b/pythonx/UltiSnips/__init__.py @@ -3,11 +3,4 @@ """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')) +from UltiSnips.snippet_manager import UltiSnips_Manager diff --git a/pythonx/UltiSnips/_vim.py b/pythonx/UltiSnips/_vim.py index 9665a2b..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 @@ -108,7 +129,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 +169,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 +198,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_proxy.py b/pythonx/UltiSnips/buffer_proxy.py new file mode 100644 index 0000000..1e007e7 --- /dev/null +++ b/pythonx/UltiSnips/buffer_proxy.py @@ -0,0 +1,224 @@ +# 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 + +from contextlib import contextmanager + + +@contextmanager +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, vstate) + old_buffer = _vim.buf + try: + _vim.buf = buffer_proxy + yield + finally: + _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: + try: + _vim.buf._disable_edits() + yield + 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, vstate): + """ + 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 + self._vstate = vstate + + 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 ' + + 'errors in snippet expansion; use special variable `snip.buffer` ' + 'for buffer modifications.\n\n' + + 'See :help UltiSnips-buffer-proxy for more info.') + + 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(line) for line in value] + changes = list(self._get_diff(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)) + self._buffer[key] = value + + self._change_tick += 1 + + 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): + """ + 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): + line = [line] + self[line_number:line_number] = [as_vimencoding(l) for l in 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): + """ + 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]) + + 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 + else: + for change in diff(before, after): + 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 + + 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: + 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): + """ + 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 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..62af17e 100644 --- a/pythonx/UltiSnips/snippet/definition/_base.py +++ b/pythonx/UltiSnips/snippet/definition/_base.py @@ -10,8 +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 SnippetUtilCursor, SnippetUtilForAction __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,82 @@ 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('snip.context = ' + self._context_code, { + 'context': None + }).context + + 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 + locals = { - 'context': context, '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': SnippetUtilCursor(current.window.cursor) } - exec(code, locals) - return context["match"] + + locals.update(additional_locals) + + snip = SnippetUtilForAction(locals) + + exec(code, {'snip': snip}) + + return snip + + def _execute_action( + self, + action, + context, + additional_locals={} + ): + mark_to_use = '`' + with _vim.save_mark(mark_to_use): + _vim.set_mark_from_pos(mark_to_use, _vim.get_cursor_pos()) + + cursor_line_before = _vim.buf.line_till_cursor + + locals = { + 'context': context, + } + + locals.update(additional_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 + 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' + ) + + return snip def has_option(self, opt): """Check if the named option is set.""" @@ -184,6 +240,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: @@ -243,6 +300,65 @@ class SnippetDefinition(object): objects alive inside of Vim.""" raise NotImplementedError() + def do_pre_expand(self, visual_content, snippets_stack): + if 'pre_expand' in self._actions: + locals = {'buffer': _vim.buf, 'visual_content': visual_content} + + snip = self._execute_action( + self._actions['pre_expand'], self._context, locals + ) + + self._context = snip.context + + return snip.cursor.is_set() + else: + return False + + def do_post_expand(self, start, end, snippets_stack): + if 'post_expand' in self._actions: + locals = { + 'snippet_start': start, + 'snippet_end': end, + 'buffer': _vim.buf + } + + snip = self._execute_action( + self._actions['post_expand'], snippets_stack[-1].context, locals + ) + + snippets_stack[-1].context = snip.context + + return snip.cursor.is_set() + else: + return False + + def do_post_jump( + self, tabstop_number, jump_direction, snippets_stack, current_snippet + ): + if 'post_jump' in self._actions: + start = current_snippet.start + end = current_snippet.end + + locals = { + 'tabstop': tabstop_number, + 'jump_direction': jump_direction, + 'tabstops': current_snippet.get_tabstops(), + 'snippet_start': start, + 'snippet_end': end, + 'buffer': _vim.buf + } + + snip = self._execute_action( + self._actions['post_jump'], current_snippet.context, locals + ) + + current_snippet.context = snip.context + + return snip.cursor.is_set() + else: + return False + + 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..f144fbf 100644 --- a/pythonx/UltiSnips/snippet_manager.py +++ b/pythonx/UltiSnips/snippet_manager.py @@ -8,6 +8,8 @@ from functools import wraps import os import platform import traceback +import vim +from contextlib import contextmanager from UltiSnips import _vim from UltiSnips._diff import diff, guess_edit @@ -18,6 +20,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_proxy import use_proxy_buffer, suspend_proxy_edits def _ask_user(a, formatted): @@ -94,6 +97,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) @@ -205,19 +211,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 +284,7 @@ class SnippetManager(object): if _vim.eval('mode()') not in 'in': return + if self._ignore_movements: self._ignore_movements = False return @@ -430,38 +440,66 @@ class SnippetManager(object): self._teardown_inner_state() def _jump(self, backwards=False): - """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: - self._current_snippet_is_done() + # we need to set 'onemore' there, because of limitations of the vim + # API regarding cursor movements; without that test + # 'CanExpandAnonSnippetInJumpActionWhileSelected' will fail + with _vim.toggle_opt('ve', 'onemore'): + """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[:] + + # 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: + snippet_for_action = self._cs + elif stack_for_post_jump: + snippet_for_action = stack_for_post_jump[-1] 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 + snippet_for_action = None + + 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() + 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: + 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, + snippet_for_action + ) + return jumped def _leaving_insert_mode(self): @@ -557,42 +595,77 @@ 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)] - if self._cs: - start = Position(_vim.buf.cursor.line, len(text_before)) - end = Position(_vim.buf.cursor.line, len(before)) + with use_proxy_buffer(self._csnippets, self._vstate): + with self._action_context(): + cursor_set_in_action = snippet.do_pre_expand( + self._visual_content.text, + self._csnippets + ) - # 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_set_in_action: + text_before = _vim.buf.line_till_cursor + before = _vim.buf.line_till_cursor - 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) + with suspend_proxy_edits(): + if self._cs: + start = Position(_vim.buf.cursor.line, len(text_before)) + end = Position(_vim.buf.cursor.line, len(before)) - self._visual_content.reset() - self._csnippets.append(si) + # 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.update_textobjects() + 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._vstate.remember_buffer(self._csnippets[0]) + self._visual_content.reset() + self._csnippets.append(si) + + si.update_textobjects() + + with use_proxy_buffer(self._csnippets, self._vstate): + with self._action_context(): + snippet.do_post_expand( + si._start, si._end, self._csnippets + ) + + self._vstate.remember_buffer(self._csnippets[0]) + + 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 - self._jump() def _try_expand(self): """Try to expand a snippet in the current place.""" @@ -682,3 +755,17 @@ 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 + +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/_base.py b/pythonx/UltiSnips/text_objects/_base.py index db18e59..6ee1745 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 and 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 5660d6e..9427c01 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.snippet_manager class _Tabs(object): @@ -30,6 +31,54 @@ 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 + + def expand_anon(self, *args, **kwargs): + UltiSnips.snippet_manager.UltiSnips_Manager.expand_anon( + *args, **kwargs + ) + self.cursor.preserve() + + +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. @@ -38,11 +87,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. @@ -149,6 +199,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': @@ -186,10 +240,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/_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/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/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..f178b40 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(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-2])" 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 """} @@ -127,3 +127,25 @@ 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" "snip.cursor" e + `!p snip.rv = str(snip.context)` + endsnippet + """} + + 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" 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..95a9216 --- /dev/null +++ b/test/test_SnippetActions.py @@ -0,0 +1,348 @@ +from test.vim_test_case import VimTestCase as _VimTest +from test.constant import * + + +class SnippetActions_PreActionModifiesBuffer(_VimTest): + files = { 'us/all.snippets': r""" + pre_expand "snip.buffer[snip.line:snip.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 "snip.buffer[snip.line+1:snip.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 "snip.buffer[snip.line] = ' '*4; snip.cursor[1] = 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): + snip.buffer[start[0]:start[0]] = ['\n'] * 2 + snip.buffer[end[0]+1:end[0]+1] = ['\n'] * 1 + endglobal + + post_expand "ensure_newlines(snip.snippet_start, snip.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): + snip.buffer[start[0]:start[0]] = ['\n'] * 2 + endglobal + + post_expand "ensure_newlines(snip.snippet_start, snip.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(): + snip.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(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(snip)" + 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(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(snip)" + 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 "snip.buffer[2] = 'debug({})'.format(snip.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(snip): + if snip.tabstop == 0: + snip.expand_anon("a($2, $1)") + endglobal + + post_jump "expand_anon(snip)" + 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(snip): + if snip.tabstop == 0: + snip.expand_anon(" // a($2, $1)") + endglobal + + post_jump "expand_anon(snip)" + 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""" + pre_expand "snip.buffer[snip.line:snip.line] = [snip.context]" + snippet i "desc" "'some context'" e + body + endsnippet + """} + 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""" + +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""" + +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""" 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' 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..f3a6e08 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."""