You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

819 lines
31 KiB
Python

#!/usr/bin/env python
# encoding: utf-8
"""Contains the SnippetManager facade used by all Vim Functions."""
from collections import defaultdict
from functools import wraps
import os
import platform
import traceback
import sys
import vim
import re
from contextlib import contextmanager
from UltiSnips import _vim
from UltiSnips._diff import diff, guess_edit
from UltiSnips.compatibility import as_unicode
from UltiSnips.position import Position
from UltiSnips.snippet.definition import UltiSnipsSnippetDefinition
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):
"""Asks the user using inputlist() and returns the selected element or
None."""
try:
rv = _vim.eval('inputlist(%s)' % _vim.escape(formatted))
if rv is None or rv == '0':
return None
rv = int(rv)
if rv > len(a):
rv = len(a)
return a[rv - 1]
except _vim.error:
# Likely "invalid expression", but might be translated. We have no way
# of knowing the exact error, therefore, we ignore all errors silently.
return None
except KeyboardInterrupt:
return None
def _ask_snippets(snippets):
"""Given a list of snippets, ask the user which one they want to use, and
return it."""
display = [as_unicode('%i: %s (%s)') % (i + 1, escape(s.description, '\\'),
escape(s.location, '\\')) for i, s in enumerate(snippets)]
return _ask_user(snippets, display)
def err_to_scratch_buffer(func):
"""Decorator that will catch any Exception that 'func' throws and displays
it in a new Vim scratch buffer."""
@wraps(func)
def wrapper(self, *args, **kwds):
try:
return func(self, *args, **kwds)
except Exception as e: # pylint: disable=bare-except
msg = \
"""An error occured. This is either a bug in UltiSnips or a bug in a
snippet definition. If you think this is a bug, please report it to
https://github.com/SirVer/ultisnips/issues/new.
Following is the full stack trace:
"""
msg += traceback.format_exc()
if hasattr(e, 'snippet_info'):
msg += "\nSnippet, caused error:\n"
msg += re.sub(
'^(?=\S)', ' ', e.snippet_info, flags=re.MULTILINE
)
# snippet_code comes from _python_code.py, it's set manually for
# providing error message with stacktrace of failed python code
# inside of the snippet.
if hasattr(e, 'snippet_code'):
_, _, tb = sys.exc_info()
tb_top = traceback.extract_tb(tb)[-1]
msg += "\nExecuted snippet code:\n"
lines = e.snippet_code.split("\n")
for number, line in enumerate(lines, 1):
msg += str(number).rjust(3)
prefix = " " if line else ""
if tb_top[1] == number:
prefix = " > "
msg += prefix + line + "\n"
# Vim sends no WinLeave msg here.
self._leaving_buffer() # pylint:disable=protected-access
_vim.new_scratch_buffer(msg)
return wrapper
# TODO(sirver): This class is still too long. It should only contain public
# facing methods, most of the private methods should be moved outside of it.
class SnippetManager(object):
"""The main entry point for all UltiSnips functionality.
All Vim functions call methods in this class.
"""
def __init__(self, expand_trigger, forward_trigger, backward_trigger):
self.expand_trigger = expand_trigger
self.forward_trigger = forward_trigger
self.backward_trigger = backward_trigger
self._inner_state_up = False
self._supertab_keys = None
self._csnippets = []
self._buffer_filetypes = defaultdict(lambda: ['all'])
self._vstate = VimState()
self._visual_content = VisualContentPreserver()
self._snippet_sources = []
self._snip_expanded_in_action = False
self._inside_action = False
self._last_inserted_char = ''
self._added_snippets_source = AddedSnippetsSource()
self.register_snippet_source('ultisnips_files', UltiSnipsFileSource())
self.register_snippet_source('added', self._added_snippets_source)
enable_snipmate = '1'
if _vim.eval("exists('g:UltiSnipsEnableSnipMate')") == '1':
enable_snipmate = _vim.eval('g:UltiSnipsEnableSnipMate')
if enable_snipmate == '1':
self.register_snippet_source('snipmate_files',
SnipMateFileSource())
self._reinit()
@err_to_scratch_buffer
def jump_forwards(self):
"""Jumps to the next tabstop."""
_vim.command('let g:ulti_jump_forwards_res = 1')
_vim.command('let &undolevels = &undolevels')
if not self._jump():
_vim.command('let g:ulti_jump_forwards_res = 0')
return self._handle_failure(self.forward_trigger)
@err_to_scratch_buffer
def jump_backwards(self):
"""Jumps to the previous tabstop."""
_vim.command('let g:ulti_jump_backwards_res = 1')
_vim.command('let &undolevels = &undolevels')
if not self._jump(True):
_vim.command('let g:ulti_jump_backwards_res = 0')
return self._handle_failure(self.backward_trigger)
@err_to_scratch_buffer
def expand(self):
"""Try to expand a snippet at the current position."""
_vim.command('let g:ulti_expand_res = 1')
if not self._try_expand():
_vim.command('let g:ulti_expand_res = 0')
self._handle_failure(self.expand_trigger)
@err_to_scratch_buffer
def expand_or_jump(self):
"""This function is used for people who wants to have the same trigger
for expansion and forward jumping.
It first tries to expand a snippet, if this fails, it tries to
jump forward.
"""
_vim.command('let g:ulti_expand_or_jump_res = 1')
rv = self._try_expand()
if not rv:
_vim.command('let g:ulti_expand_or_jump_res = 2')
rv = self._jump()
if not rv:
_vim.command('let g:ulti_expand_or_jump_res = 0')
self._handle_failure(self.expand_trigger)
@err_to_scratch_buffer
def snippets_in_current_scope(self):
"""Returns the snippets that could be expanded to Vim as a global
variable."""
before = _vim.buf.line_till_cursor
snippets = self._snips(before, True)
# Sort snippets alphabetically
snippets.sort(key=lambda x: x.trigger)
for snip in snippets:
description = snip.description[snip.description.find(snip.trigger) +
len(snip.trigger) + 2:]
key = as_unicode(snip.trigger)
description = as_unicode(description)
# remove surrounding "" or '' in snippet description if it exists
if len(description) > 2:
if (description[0] == description[-1] and
description[0] in "'\""):
description = description[1:-1]
_vim.command(as_unicode(
"let g:current_ulti_dict['{key}'] = '{val}'").format(
key=key.replace("'", "''"),
val=description.replace("'", "''")))
@err_to_scratch_buffer
def list_snippets(self):
"""Shows the snippets that could be expanded to the User and let her
select one."""
before = _vim.buf.line_till_cursor
snippets = self._snips(before, True)
if len(snippets) == 0:
self._handle_failure(self.backward_trigger)
return True
# Sort snippets alphabetically
snippets.sort(key=lambda x: x.trigger)
if not snippets:
return True
snippet = _ask_snippets(snippets)
if not snippet:
return True
self._do_snippet(snippet, before)
return True
@err_to_scratch_buffer
def add_snippet(self, trigger, value, description,
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, actions))
@err_to_scratch_buffer
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, actions)
if not trigger or snip.matches(before):
self._do_snippet(snip, before)
return True
else:
return False
def register_snippet_source(self, name, snippet_source):
"""Registers a new 'snippet_source' with the given 'name'.
The given class must be an instance of SnippetSource. This
source will be queried for snippets.
"""
self._snippet_sources.append((name, snippet_source))
def unregister_snippet_source(self, name):
"""Unregister the source with the given 'name'.
Does nothing if it is not registered.
"""
for index, (source_name, _) in enumerate(self._snippet_sources):
if name == source_name:
self._snippet_sources = self._snippet_sources[:index] + \
self._snippet_sources[index + 1:]
break
def reset_buffer_filetypes(self):
"""Reset the filetypes for the current buffer."""
if _vim.buf.number in self._buffer_filetypes:
del self._buffer_filetypes[_vim.buf.number]
def add_buffer_filetypes(self, ft):
"""Checks for changes in the list of snippet files or the contents of
the snippet files and reloads them if necessary."""
buf_fts = self._buffer_filetypes[_vim.buf.number]
idx = -1
for ft in ft.split('.'):
ft = ft.strip()
if not ft:
continue
try:
idx = buf_fts.index(ft)
except ValueError:
self._buffer_filetypes[_vim.buf.number].insert(idx + 1, ft)
idx += 1
@err_to_scratch_buffer
def _cursor_moved(self):
"""Called whenever the cursor moved."""
if not self._csnippets and self._inner_state_up:
self._teardown_inner_state()
self._vstate.remember_position()
if _vim.eval('mode()') not in 'in':
return
if self._ignore_movements:
self._ignore_movements = False
return
if self._csnippets:
cstart = self._csnippets[0].start.line
cend = self._csnippets[0].end.line + \
self._vstate.diff_in_buffer_length
ct = _vim.buf[cstart:cend + 1]
lt = self._vstate.remembered_buffer
pos = _vim.buf.cursor
lt_span = [0, len(lt)]
ct_span = [0, len(ct)]
initial_line = cstart
# Cut down on lines searched for changes. Start from behind and
# remove all equal lines. Then do the same from the front.
if lt and ct:
while (lt[lt_span[1] - 1] == ct[ct_span[1] - 1] and
self._vstate.ppos.line < initial_line + lt_span[1] - 1 and
pos.line < initial_line + ct_span[1] - 1 and
(lt_span[0] < lt_span[1]) and
(ct_span[0] < ct_span[1])):
ct_span[1] -= 1
lt_span[1] -= 1
while (lt_span[0] < lt_span[1] and
ct_span[0] < ct_span[1] and
lt[lt_span[0]] == ct[ct_span[0]] and
self._vstate.ppos.line >= initial_line and
pos.line >= initial_line):
ct_span[0] += 1
lt_span[0] += 1
initial_line += 1
ct_span[0] = max(0, ct_span[0] - 1)
lt_span[0] = max(0, lt_span[0] - 1)
initial_line = max(cstart, initial_line - 1)
lt = lt[lt_span[0]:lt_span[1]]
ct = ct[ct_span[0]:ct_span[1]]
try:
rv, es = guess_edit(initial_line, lt, ct, self._vstate)
if not rv:
lt = '\n'.join(lt)
ct = '\n'.join(ct)
es = diff(lt, ct, initial_line)
self._csnippets[0].replay_user_edits(es, self._ctab)
except IndexError:
# Rather do nothing than throwing an error. It will be correct
# most of the time
pass
self._check_if_still_inside_snippet()
if self._csnippets:
self._csnippets[0].update_textobjects()
self._vstate.remember_buffer(self._csnippets[0])
def _setup_inner_state(self):
"""Map keys and create autocommands that should only be defined when a
snippet is active."""
if self._inner_state_up:
return
if self.expand_trigger != self.forward_trigger:
_vim.command('inoremap <buffer> <silent> ' + self.forward_trigger +
' <C-R>=UltiSnips#JumpForwards()<cr>')
_vim.command('snoremap <buffer> <silent> ' + self.forward_trigger +
' <Esc>:call UltiSnips#JumpForwards()<cr>')
_vim.command('inoremap <buffer> <silent> ' + self.backward_trigger +
' <C-R>=UltiSnips#JumpBackwards()<cr>')
_vim.command('snoremap <buffer> <silent> ' + self.backward_trigger +
' <Esc>:call UltiSnips#JumpBackwards()<cr>')
# Setup the autogroups.
_vim.command('augroup UltiSnips')
_vim.command('autocmd!')
_vim.command('autocmd CursorMovedI * call UltiSnips#CursorMoved()')
_vim.command('autocmd CursorMoved * call UltiSnips#CursorMoved()')
_vim.command(
'autocmd InsertLeave * call UltiSnips#LeavingInsertMode()')
_vim.command('autocmd BufLeave * call UltiSnips#LeavingBuffer()')
_vim.command(
'autocmd CmdwinEnter * call UltiSnips#LeavingBuffer()')
_vim.command(
'autocmd CmdwinLeave * call UltiSnips#LeavingBuffer()')
# Also exit the snippet when we enter a unite complete buffer.
_vim.command('autocmd Filetype unite call UltiSnips#LeavingBuffer()')
_vim.command('augroup END')
_vim.command('silent doautocmd <nomodeline> User UltiSnipsEnterFirstSnippet')
self._inner_state_up = True
def _teardown_inner_state(self):
"""Reverse _setup_inner_state."""
if not self._inner_state_up:
return
try:
_vim.command('silent doautocmd <nomodeline> User UltiSnipsExitLastSnippet')
if self.expand_trigger != self.forward_trigger:
_vim.command('iunmap <buffer> %s' % self.forward_trigger)
_vim.command('sunmap <buffer> %s' % self.forward_trigger)
_vim.command('iunmap <buffer> %s' % self.backward_trigger)
_vim.command('sunmap <buffer> %s' % self.backward_trigger)
_vim.command('augroup UltiSnips')
_vim.command('autocmd!')
_vim.command('augroup END')
self._inner_state_up = False
except _vim.error:
# This happens when a preview window was opened. This issues
# CursorMoved, but not BufLeave. We have no way to unmap, until we
# are back in our buffer
pass
@err_to_scratch_buffer
def _save_last_visual_selection(self):
"""This is called when the expand trigger is pressed in visual mode.
Our job is to remember everything between '< and '> and pass it on to.
${VISUAL} in case it will be needed.
"""
self._visual_content.conserve()
def _leaving_buffer(self):
"""Called when the user switches tabs/windows/buffers.
It basically means that all snippets must be properly
terminated.
"""
while len(self._csnippets):
self._current_snippet_is_done()
self._reinit()
def _reinit(self):
"""Resets transient state."""
self._ctab = None
self._ignore_movements = False
def _check_if_still_inside_snippet(self):
"""Checks if the cursor is outside of the current snippet."""
if self._cs and (
not self._cs.start <= _vim.buf.cursor <= self._cs.end
):
self._current_snippet_is_done()
self._reinit()
self._check_if_still_inside_snippet()
def _current_snippet_is_done(self):
"""The current snippet should be terminated."""
self._csnippets.pop()
if not self._csnippets:
self._teardown_inner_state()
def _jump(self, backwards=False):
# 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:
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):
"""Called whenever we leave the insert mode."""
self._vstate.restore_unnamed_register()
def _handle_failure(self, trigger):
"""Mainly make sure that we play well with SuperTab."""
if trigger.lower() == '<tab>':
feedkey = '\\' + trigger
elif trigger.lower() == '<s-tab>':
feedkey = '\\' + trigger
else:
feedkey = None
mode = 'n'
if not self._supertab_keys:
if _vim.eval("exists('g:SuperTabMappingForward')") != '0':
self._supertab_keys = (
_vim.eval('g:SuperTabMappingForward'),
_vim.eval('g:SuperTabMappingBackward'),
)
else:
self._supertab_keys = ['', '']
for idx, sttrig in enumerate(self._supertab_keys):
if trigger.lower() == sttrig.lower():
if idx == 0:
feedkey = r"\<Plug>SuperTabForward"
mode = 'n'
elif idx == 1:
feedkey = r"\<Plug>SuperTabBackward"
mode = 'p'
# Use remap mode so SuperTab mappings will be invoked.
break
if (feedkey == r"\<Plug>SuperTabForward" or
feedkey == r"\<Plug>SuperTabBackward"):
_vim.command('return SuperTab(%s)' % _vim.escape(mode))
elif feedkey:
_vim.command('return %s' % _vim.escape(feedkey))
def _snips(self, before, partial, autotrigger_only=False):
"""Returns all the snippets for the given text before the cursor.
If partial is True, then get also return partial matches.
"""
filetypes = self._buffer_filetypes[_vim.buf.number][::-1]
matching_snippets = defaultdict(list)
clear_priority = None
cleared = {}
for _, source in self._snippet_sources:
source.ensure(filetypes, cached=autotrigger_only)
# Collect cleared information from sources.
for _, source in self._snippet_sources:
sclear_priority = source.get_clear_priority(filetypes)
if sclear_priority is not None and (clear_priority is None
or sclear_priority > clear_priority):
clear_priority = sclear_priority
for key, value in source.get_cleared(filetypes).items():
if key not in cleared or value > cleared[key]:
cleared[key] = value
for _, source in self._snippet_sources:
possible_snippets = source.get_snippets(
filetypes,
before,
partial,
autotrigger_only
)
for snippet in possible_snippets:
if ((clear_priority is None or snippet.priority > clear_priority)
and (snippet.trigger not in cleared or
snippet.priority > cleared[snippet.trigger])):
matching_snippets[snippet.trigger].append(snippet)
if not matching_snippets:
return []
# Now filter duplicates and only keep the one with the highest
# priority.
snippets = []
for snippets_with_trigger in matching_snippets.values():
highest_priority = max(s.priority for s in snippets_with_trigger)
snippets.extend(s for s in snippets_with_trigger
if s.priority == highest_priority)
# For partial matches we are done, but if we want to expand a snippet,
# we have to go over them again and only keep those with the maximum
# priority.
if partial:
return snippets
highest_priority = max(s.priority for s in snippets)
return [s for s in snippets if s.priority == highest_priority]
def _do_snippet(self, snippet, before):
"""Expands the given snippet, and handles everything that needs to be
done with it."""
self._setup_inner_state()
self._snip_expanded_in_action = False
# Adjust before, maybe the trigger is not the complete word
text_before = before
if snippet.matched:
text_before = before[:-len(snippet.matched)]
with use_proxy_buffer(self._csnippets, self._vstate):
with self._action_context():
cursor_set_in_action = snippet.do_pre_expand(
self._visual_content.text,
self._csnippets
)
if cursor_set_in_action:
text_before = _vim.buf.line_till_cursor
before = _vim.buf.line_till_cursor
with suspend_proxy_edits():
if self._cs:
start = Position(_vim.buf.cursor.line, len(text_before))
end = Position(_vim.buf.cursor.line, len(before))
# 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)
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
def _try_expand(self, autotrigger_only=False):
"""Try to expand a snippet in the current place."""
before = _vim.buf.line_till_cursor
snippets = self._snips(before, False, autotrigger_only)
if snippets:
# prefer snippets with context if any
snippets_with_context = [s for s in snippets if s.context]
if snippets_with_context:
snippets = snippets_with_context
if not snippets:
# No snippet found
return False
_vim.command('let &undolevels = &undolevels')
if len(snippets) == 1:
snippet = snippets[0]
else:
snippet = _ask_snippets(snippets)
if not snippet:
return True
self._do_snippet(snippet, before)
_vim.command('let &undolevels = &undolevels')
return True
@property
def _cs(self):
"""The current snippet or None."""
if not len(self._csnippets):
return None
return self._csnippets[-1]
def _file_to_edit(self, requested_ft, bang): # pylint: disable=no-self-use
"""Returns a file to be edited for the given requested_ft.
If 'bang' is
empty only private files in g:UltiSnipsSnippetsDir are considered,
otherwise all files are considered and the user gets to choose.
"""
# This method is not using self, but is called by UltiSnips.vim and is
# therefore in this class because it is the facade to Vim.
potentials = set()
if _vim.eval("exists('g:UltiSnipsSnippetsDir')") == '1':
snippet_dir = _vim.eval('g:UltiSnipsSnippetsDir')
else:
home = _vim.eval('$HOME')
if platform.system() == 'Windows':
snippet_dir = os.path.join(home, 'vimfiles', 'UltiSnips')
elif _vim.eval("has('nvim')") == '1':
xdg_home_config = _vim.eval('$XDG_CONFIG_HOME') or os.path.join(home, ".config")
snippet_dir = os.path.join(xdg_home_config, 'nvim', 'UltiSnips')
else:
snippet_dir = os.path.join(home, '.vim', 'UltiSnips')
filetypes = []
if requested_ft:
filetypes.append(requested_ft)
else:
if bang:
filetypes.extend(self._buffer_filetypes[_vim.buf.number])
else:
filetypes.append(self._buffer_filetypes[_vim.buf.number][0])
for ft in filetypes:
potentials.update(find_snippet_files(ft, snippet_dir))
potentials.add(os.path.join(snippet_dir,
ft + '.snippets'))
if bang:
potentials.update(find_all_snippet_files(ft))
potentials = set(os.path.realpath(os.path.expanduser(p))
for p in potentials)
if len(potentials) > 1:
files = sorted(potentials)
formatted = [as_unicode('%i: %s') % (i, escape(fn, '\\')) for
i, fn in enumerate(files, 1)]
file_to_edit = _ask_user(files, formatted)
if file_to_edit is None:
return ''
else:
file_to_edit = potentials.pop()
dirname = os.path.dirname(file_to_edit)
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
@err_to_scratch_buffer
def _track_change(self):
inserted_char = _vim.eval('v:char')
try:
if inserted_char == '':
before = _vim.buf.line_till_cursor
if before and before[-1] == self._last_inserted_char:
self._try_expand(autotrigger_only=True)
finally:
self._last_inserted_char = inserted_char
UltiSnips_Manager = SnippetManager( # pylint:disable=invalid-name
vim.eval('g:UltiSnipsExpandTrigger'),
vim.eval('g:UltiSnipsJumpForwardTrigger'),
vim.eval('g:UltiSnipsJumpBackwardTrigger'))