Fixed vim and zsh

This commit is contained in:
2018-04-05 13:06:54 +02:00
parent f9db886bd3
commit 0331f6518a
2009 changed files with 256303 additions and 0 deletions

View File

@ -0,0 +1,6 @@
#!/usr/bin/env python
# encoding: utf-8
"""Entry point for all thinks UltiSnips."""
from UltiSnips.snippet_manager import UltiSnips_Manager

View File

@ -0,0 +1,226 @@
#!/usr/bin/env python
# encoding: utf-8
"""Commands to compare text objects and to guess how to transform from one to
another."""
from collections import defaultdict
import sys
from UltiSnips import _vim
from UltiSnips.position import Position
def is_complete_edit(initial_line, original, wanted, cmds):
"""Returns true if 'original' is changed to 'wanted' with the edit commands
in 'cmds'.
Initial line is to change the line numbers in 'cmds'.
"""
buf = original[:]
for cmd in cmds:
ctype, line, col, char = cmd
line -= initial_line
if ctype == 'D':
if char != '\n':
buf[line] = buf[line][:col] + buf[line][col + len(char):]
else:
if line + 1 < len(buf):
buf[line] = buf[line] + buf[line + 1]
del buf[line + 1]
else:
del buf[line]
elif ctype == 'I':
buf[line] = buf[line][:col] + char + buf[line][col:]
buf = '\n'.join(buf).split('\n')
return (len(buf) == len(wanted) and
all(j == k for j, k in zip(buf, wanted)))
def guess_edit(initial_line, last_text, current_text, vim_state):
"""Try to guess what the user might have done by heuristically looking at
cursor movement, number of changed lines and if they got longer or shorter.
This will detect most simple movements like insertion, deletion of a line
or carriage return. 'initial_text' is the index of where the comparison
starts, 'last_text' is the last text of the snippet, 'current_text' is the
current text of the snippet and 'vim_state' is the cached vim state.
Returns (True, edit_cmds) when the edit could be guessed, (False,
None) otherwise.
"""
if not len(last_text) and not len(current_text):
return True, ()
pos = vim_state.pos
ppos = vim_state.ppos
# All text deleted?
if (len(last_text) and
(not current_text or
(len(current_text) == 1 and not current_text[0]))
):
es = []
if not current_text:
current_text = ['']
for i in last_text:
es.append(('D', initial_line, 0, i))
es.append(('D', initial_line, 0, '\n'))
es.pop() # Remove final \n because it is not really removed
if is_complete_edit(initial_line, last_text, current_text, es):
return True, es
if ppos.mode == 'v': # Maybe selectmode?
sv = list(map(int, _vim.eval("""getpos("'<")""")))
sv = Position(sv[1] - 1, sv[2] - 1)
ev = list(map(int, _vim.eval("""getpos("'>")""")))
ev = Position(ev[1] - 1, ev[2] - 1)
if 'exclusive' in _vim.eval('&selection'):
ppos.col -= 1 # We want to be inclusive, sorry.
ev.col -= 1
es = []
if sv.line == ev.line:
es.append(('D', sv.line, sv.col,
last_text[sv.line - initial_line][sv.col:ev.col + 1]))
if sv != pos and sv.line == pos.line:
es.append(('I', sv.line, sv.col,
current_text[sv.line - initial_line][sv.col:pos.col + 1]))
if is_complete_edit(initial_line, last_text, current_text, es):
return True, es
if pos.line == ppos.line:
if len(last_text) == len(current_text): # Movement only in one line
llen = len(last_text[ppos.line - initial_line])
clen = len(current_text[pos.line - initial_line])
if ppos < pos and clen > llen: # maybe only chars have been added
es = (
('I', ppos.line, ppos.col,
current_text[ppos.line - initial_line]
[ppos.col:pos.col]),
)
if is_complete_edit(initial_line, last_text, current_text, es):
return True, es
if clen < llen:
if ppos == pos: # 'x' or DEL or dt or something
es = (
('D', pos.line, pos.col,
last_text[ppos.line - initial_line]
[ppos.col:ppos.col + (llen - clen)]),
)
if is_complete_edit(initial_line, last_text,
current_text, es):
return True, es
if pos < ppos: # Backspacing or dT dF?
es = (
('D', pos.line, pos.col,
last_text[pos.line - initial_line]
[pos.col:pos.col + llen - clen]),
)
if is_complete_edit(initial_line, last_text,
current_text, es):
return True, es
elif len(current_text) < len(last_text):
# where some lines deleted? (dd or so)
es = []
for i in range(len(last_text) - len(current_text)):
es.append(('D', pos.line, 0,
last_text[pos.line - initial_line + i]))
es.append(('D', pos.line, 0, '\n'))
if is_complete_edit(initial_line, last_text,
current_text, es):
return True, es
else:
# Movement in more than one line
if ppos.line + 1 == pos.line and pos.col == 0: # Carriage return?
es = (('I', ppos.line, ppos.col, '\n'),)
if is_complete_edit(initial_line, last_text,
current_text, es):
return True, es
return False, None
def diff(a, b, sline=0):
"""
Return a list of deletions and insertions that will turn 'a' into 'b'. This
is done by traversing an implicit edit graph and searching for the shortest
route. The basic idea is as follows:
- Matching a character is free as long as there was no
deletion/insertion before. Then, matching will be seen as delete +
insert [1].
- Deleting one character has the same cost everywhere. Each additional
character costs only have of the first deletion.
- Insertion is cheaper the earlier it happens. The first character is
more expensive that any later [2].
[1] This is that world -> aolsa will be "D" world + "I" aolsa instead of
"D" w , "D" rld, "I" a, "I" lsa
[2] This is that "hello\n\n" -> "hello\n\n\n" will insert a newline after
hello and not after \n
"""
d = defaultdict(list) # pylint:disable=invalid-name
seen = defaultdict(lambda: sys.maxsize)
d[0] = [(0, 0, sline, 0, ())]
cost = 0
deletion_cost = len(a) + len(b)
insertion_cost = len(a) + len(b)
while True:
while len(d[cost]):
x, y, line, col, what = d[cost].pop()
if a[x:] == b[y:]:
return what
if x < len(a) and y < len(b) and a[x] == b[y]:
ncol = col + 1
nline = line
if a[x] == '\n':
ncol = 0
nline += 1
lcost = cost + 1
if (what and what[-1][0] == 'D' and what[-1][1] == line and
what[-1][2] == col and a[x] != '\n'):
# Matching directly after a deletion should be as costly as
# DELETE + INSERT + a bit
lcost = (deletion_cost + insertion_cost) * 1.5
if seen[x + 1, y + 1] > lcost:
d[lcost].append((x + 1, y + 1, nline, ncol, what))
seen[x + 1, y + 1] = lcost
if y < len(b): # INSERT
ncol = col + 1
nline = line
if b[y] == '\n':
ncol = 0
nline += 1
if (what and what[-1][0] == 'I' and what[-1][1] == nline and
what[-1][2] + len(what[-1][-1]) == col and b[y] != '\n' and
seen[x, y + 1] > cost + (insertion_cost + ncol) // 2
):
seen[x, y + 1] = cost + (insertion_cost + ncol) // 2
d[cost + (insertion_cost + ncol) // 2].append(
(x, y + 1, line, ncol, what[:-1] + (
('I', what[-1][1], what[-1][2],
what[-1][-1] + b[y]),)
)
)
elif seen[x, y + 1] > cost + insertion_cost + ncol:
seen[x, y + 1] = cost + insertion_cost + ncol
d[cost + ncol + insertion_cost].append((x, y + 1, nline, ncol,
what + (('I', line, col, b[y]),))
)
if x < len(a): # DELETE
if (what and what[-1][0] == 'D' and what[-1][1] == line and
what[-1][2] == col and a[x] != '\n' and
what[-1][-1] != '\n' and
seen[x + 1, y] > cost + deletion_cost // 2
):
seen[x + 1, y] = cost + deletion_cost // 2
d[cost + deletion_cost // 2].append(
(x + 1, y, line, col, what[:-1] + (
('D', line, col, what[-1][-1] + a[x]),))
)
elif seen[x + 1, y] > cost + deletion_cost:
seen[x + 1, y] = cost + deletion_cost
d[cost + deletion_cost].append((x + 1, y, line, col, what +
(('D', line, col, a[x]),))
)
cost += 1

View File

@ -0,0 +1,309 @@
#!/usr/bin/env python
# encoding: utf-8
"""Wrapper functionality around the functions we need from Vim."""
import re
import vim # pylint:disable=import-error
from vim import error # pylint:disable=import-error,unused-import
from UltiSnips.compatibility import col2byte, byte2col, \
as_unicode, as_vimencoding
from UltiSnips.position import Position
from contextlib import contextmanager
class VimBuffer(object):
"""Wrapper around the current Vim buffer."""
def __getitem__(self, idx):
if isinstance(idx, slice): # Py3
return self.__getslice__(idx.start, idx.stop)
rv = vim.current.buffer[idx]
return as_unicode(rv)
def __getslice__(self, i, j): # pylint:disable=no-self-use
rv = vim.current.buffer[i:j]
return [as_unicode(l) for l in rv]
def __setitem__(self, idx, text):
if isinstance(idx, slice): # Py3
return self.__setslice__(idx.start, idx.stop, text)
vim.current.buffer[idx] = as_vimencoding(text)
def __setslice__(self, i, j, text): # pylint:disable=no-self-use
vim.current.buffer[i:j] = [as_vimencoding(l) for l in text]
def __len__(self):
return len(vim.current.buffer)
@property
def line_till_cursor(self): # pylint:disable=no-self-use
"""Returns the text before the cursor."""
_, col = self.cursor
return as_unicode(vim.current.line)[:col]
@property
def number(self): # pylint:disable=no-self-use
"""The bufnr() of the current buffer."""
return vim.current.buffer.number
@property
def filetypes(self):
return [ft for ft in vim.eval('&filetype').split('.') if ft]
@property
def cursor(self): # pylint:disable=no-self-use
"""The current windows cursor.
Note that this is 0 based in col and 0 based in line which is
different from Vim's cursor.
"""
line, nbyte = vim.current.window.cursor
col = byte2col(line, nbyte)
return Position(line - 1, col)
@cursor.setter
def cursor(self, pos): # pylint:disable=no-self-use
"""See getter."""
nbyte = col2byte(pos.line + 1, pos.col)
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 {0}={1}'.format(name, new_value))
try:
yield
finally:
command('set {0}={1}'.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
dicts, lists and strings."""
def conv(obj):
"""Convert obj."""
if isinstance(obj, list):
rv = as_unicode('[' + ','.join(conv(o) for o in obj) + ']')
elif isinstance(obj, dict):
rv = as_unicode('{' + ','.join([
'%s:%s' % (conv(key), conv(value))
for key, value in obj.iteritems()]) + '}')
else:
rv = as_unicode('"%s"') % as_unicode(obj).replace('"', '\\"')
return rv
return conv(inp)
def command(cmd):
"""Wraps vim.command."""
return as_unicode(vim.command(as_vimencoding(cmd)))
def eval(text):
"""Wraps vim.eval."""
rv = vim.eval(as_vimencoding(text))
if not isinstance(rv, (dict, list)):
return as_unicode(rv)
return rv
def bindeval(text):
"""Wraps vim.bindeval."""
rv = vim.bindeval(as_vimencoding(text))
if not isinstance(rv, (dict, list)):
return as_unicode(rv)
return rv
def feedkeys(keys, mode='n'):
"""Wrapper around vim's feedkeys function.
Mainly for convenience.
"""
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):
"""Create a new scratch buffer with the text given."""
vim.command('botright new')
vim.command('set ft=')
vim.command('set buftype=nofile')
vim.current.buffer[:] = text.splitlines()
feedkeys(r"\<Esc>")
def virtual_position(line, col):
"""Runs the position through virtcol() and returns the result."""
nbytes = col2byte(line, col)
return line, int(eval('virtcol([%d, %d])' % (line, nbytes)))
def select(start, end):
"""Select the span in Select mode."""
_unmap_select_mode_mapping()
selection = eval('&selection')
col = col2byte(start.line + 1, start.col)
vim.current.window.cursor = start.line + 1, col
mode = eval('mode()')
move_cmd = ''
if mode != 'n':
move_cmd += r"\<Esc>"
if start == end:
# Zero Length Tabstops, use 'i' or 'a'.
if col == 0 or mode not in 'i' and \
col < len(buf[start.line]):
move_cmd += 'i'
else:
move_cmd += 'a'
else:
# Non zero length, use Visual selection.
move_cmd += 'v'
if 'inclusive' in selection:
if end.col == 0:
move_cmd += '%iG$' % end.line
else:
move_cmd += '%iG%i|' % virtual_position(end.line + 1, end.col)
elif 'old' in selection:
move_cmd += '%iG%i|' % virtual_position(end.line + 1, end.col)
else:
move_cmd += '%iG%i|' % virtual_position(end.line + 1, end.col + 1)
move_cmd += 'o%iG%i|o\\<c-g>' % virtual_position(
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(\"{0}\", {1})".format(name, pos))
def _get_pos(name):
return eval("getpos(\"{0}\")".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.
Removes select mode mappings that can actually be typed by the user
(ie, ignores things like <Plug>).
"""
if int(eval('g:UltiSnipsRemoveSelectModeMappings')):
ignores = eval('g:UltiSnipsMappingsToIgnore') + ['UltiSnips']
for option in ('<buffer>', ''):
# Put all smaps into a var, and then read the var
command(r"redir => _tmp_smaps | silent smap %s " % option +
'| redir END')
# Check if any mappings where found
if hasattr(vim, 'bindeval'):
# Safer to use bindeval, if it exists, because it can deal with
# non-UTF-8 characters in mappings; see GH #690.
all_maps = bindeval(r"_tmp_smaps")
else:
all_maps = eval(r"_tmp_smaps")
all_maps = list(filter(len, all_maps.splitlines()))
if len(all_maps) == 1 and all_maps[0][0] not in ' sv':
# "No maps found". String could be localized. Hopefully
# it doesn't start with any of these letters in any
# language
continue
# Only keep mappings that should not be ignored
maps = [m for m in all_maps if
not any(i in m for i in ignores) and len(m.strip())]
for map in maps:
# The first three chars are the modes, that might be listed.
# We are not interested in them here.
trig = map[3:].split()[0] if len(
map[3:].split()) != 0 else None
if trig is None:
continue
# The bar separates commands
if trig[-1] == '|':
trig = trig[:-1] + '<Bar>'
# Special ones
if trig[0] == '<':
add = False
# Only allow these
for valid in ['Tab', 'NL', 'CR', 'C-Tab', 'BS']:
if trig == '<%s>' % valid:
add = True
if not add:
continue
# UltiSnips remaps <BS>. Keep this around.
if trig == '<BS>':
continue
# Actually unmap it
try:
command('silent! sunmap %s %s' % (option, trig))
except: # pylint:disable=bare-except
# Bug 908139: ignore unmaps that fail because of
# unprintable characters. This is not ideal because we
# will not be able to unmap lhs with any unprintable
# character. If the lhs stats with a printable
# character this will leak to the user when he tries to
# type this character as a first in a selected tabstop.
# This case should be rare enough to not bother us
# though.
pass

View File

@ -0,0 +1,229 @@
# 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], True)
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], True)
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
change_type, line_number, column_number, change_text = change[0:4]
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_type == 'D':
direction = -1
diff = Position(direction, 0)
if len(change) != 5:
diff = Position(0, direction * len(change_text))
print(change, diff)
self._snippets_stack[0]._move(
Position(line_number, column_number),
diff
)
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[0:4])
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

View File

@ -0,0 +1,101 @@
#!/usr/bin/env python
# encoding: utf-8
"""This file contains compatibility code to stay compatible with as many python
versions as possible."""
import sys
import vim # pylint:disable=import-error
if sys.version_info >= (3, 0):
def _vim_dec(string):
"""Decode 'string' using &encoding."""
# We don't have the luxury here of failing, everything
# falls apart if we don't return a bytearray from the
# passed in string
return string.decode(vim.eval('&encoding'), 'replace')
def _vim_enc(bytearray):
"""Encode 'string' using &encoding."""
# We don't have the luxury here of failing, everything
# falls apart if we don't return a string from the passed
# in bytearray
return bytearray.encode(vim.eval('&encoding'), 'replace')
def open_ascii_file(filename, mode):
"""Opens a file in "r" mode."""
return open(filename, mode, encoding='utf-8')
def col2byte(line, col):
"""Convert a valid column index into a byte index inside of vims
buffer."""
# We pad the line so that selecting the +1 st column still works.
pre_chars = (vim.current.buffer[line - 1] + ' ')[:col]
return len(_vim_enc(pre_chars))
def byte2col(line, nbyte):
"""Convert a column into a byteidx suitable for a mark or cursor
position inside of vim."""
line = vim.current.buffer[line - 1]
raw_bytes = _vim_enc(line)[:nbyte]
return len(_vim_dec(raw_bytes))
def as_unicode(string):
"""Return 'string' as unicode instance."""
if isinstance(string, bytes):
return _vim_dec(string)
return str(string)
def as_vimencoding(string):
"""Return 'string' as Vim internal encoding."""
return string
else:
import warnings
warnings.filterwarnings('ignore', category=DeprecationWarning)
def _vim_dec(string):
"""Decode 'string' using &encoding."""
try:
return string.decode(vim.eval('&encoding'))
except UnicodeDecodeError:
# At least we tried. There might be some problems down the road now
return string
def _vim_enc(string):
"""Encode 'string' using &encoding."""
try:
return string.encode(vim.eval('&encoding'))
except UnicodeDecodeError:
return string
except UnicodeEncodeError:
return string
def open_ascii_file(filename, mode):
"""Opens a file in "r" mode."""
return open(filename, mode)
def col2byte(line, col):
"""Convert a valid column index into a byte index inside of vims
buffer."""
# We pad the line so that selecting the +1 st column still works.
pre_chars = _vim_dec(vim.current.buffer[line - 1] + ' ')[:col]
return len(_vim_enc(pre_chars))
def byte2col(line, nbyte):
"""Convert a column into a byteidx suitable for a mark or cursor
position inside of vim."""
line = vim.current.buffer[line - 1]
if nbyte >= len(line): # This is beyond end of line
return nbyte
return len(_vim_dec(line[:nbyte]))
def as_unicode(string):
"""Return 'string' as unicode instance."""
if isinstance(string, str):
return _vim_dec(string)
return unicode(string)
def as_vimencoding(string):
"""Return 'string' as unicode instance."""
return _vim_enc(string)

View File

@ -0,0 +1,49 @@
#!/usr/bin/env python
# encoding: utf-8
"""Convenience methods that help with debugging.
They should never be used in production code.
"""
import sys
from UltiSnips.compatibility import as_unicode
DUMP_FILENAME = '/tmp/file.txt' if not sys.platform.lower().startswith('win') \
else 'C:/windows/temp/ultisnips.txt'
with open(DUMP_FILENAME, 'w'):
pass # clears the file
def echo_to_hierarchy(text_object):
"""Outputs the given 'text_object' and its children hierarchically."""
# pylint:disable=protected-access
parent = text_object
while parent._parent:
parent = parent._parent
def _do_print(text_object, indent=''):
"""prints recursively."""
debug(indent + as_unicode(text_object))
try:
for child in text_object._children:
_do_print(child, indent=indent + ' ')
except AttributeError:
pass
_do_print(parent)
def debug(msg):
"""Dumb 'msg' into the debug file."""
msg = as_unicode(msg)
with open(DUMP_FILENAME, 'ab') as dump_file:
dump_file.write((msg + '\n').encode('utf-8'))
def print_stack():
"""Dump a stack trace into the debug file."""
import traceback
with open(DUMP_FILENAME, 'ab') as dump_file:
traceback.print_stack(file=dump_file)

View File

@ -0,0 +1,51 @@
# coding=utf8
from functools import wraps
import traceback
import re
import sys
from UltiSnips import _vim
def wrap(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.
if hasattr(self, '_leaving_buffer'):
self._leaving_buffer() # pylint:disable=protected-access
_vim.new_scratch_buffer(msg)
return wrapper

View File

@ -0,0 +1,42 @@
#!/usr/bin/env python
# encoding: utf-8
"""See module doc."""
from UltiSnips import _vim
class IndentUtil(object):
"""Utility class for dealing properly with indentation."""
def __init__(self):
self.reset()
def reset(self):
"""Gets the spacing properties from Vim."""
self.shiftwidth = int(
_vim.eval("exists('*shiftwidth') ? shiftwidth() : &shiftwidth"))
self._expandtab = (_vim.eval('&expandtab') == '1')
self._tabstop = int(_vim.eval('&tabstop'))
def ntabs_to_proper_indent(self, ntabs):
"""Convert 'ntabs' number of tabs to the proper indent prefix."""
line_ind = ntabs * self.shiftwidth * ' '
line_ind = self.indent_to_spaces(line_ind)
line_ind = self.spaces_to_indent(line_ind)
return line_ind
def indent_to_spaces(self, indent):
"""Converts indentation to spaces respecting Vim settings."""
indent = indent.expandtabs(self._tabstop)
right = (len(indent) - len(indent.rstrip(' '))) * ' '
indent = indent.replace(' ', '')
indent = indent.replace('\t', ' ' * self._tabstop)
return indent + right
def spaces_to_indent(self, indent):
"""Converts spaces to proper indentation respecting Vim settings."""
if not self._expandtab:
indent = indent.replace(' ' * self._tabstop, '\t')
return indent

View File

@ -0,0 +1,77 @@
#!/usr/bin/env python
# encoding: utf-8
"""Represents a Position in a text file: (0 based line index, 0 based column
index) and provides methods for moving them around."""
class Position(object):
"""See module docstring."""
def __init__(self, line, col):
self.line = line
self.col = col
def move(self, pivot, delta):
"""'pivot' is the position of the first changed character, 'delta' is
how text after it moved."""
if self < pivot:
return
if delta.line == 0:
if self.line == pivot.line:
self.col += delta.col
elif delta.line > 0:
if self.line == pivot.line:
self.col += delta.col - pivot.col
self.line += delta.line
else:
self.line += delta.line
if self.line == pivot.line:
self.col += - delta.col + pivot.col
def delta(self, pos):
"""Returns the difference that the cursor must move to come from 'pos'
to us."""
assert isinstance(pos, Position)
if self.line == pos.line:
return Position(0, self.col - pos.col)
else:
if self > pos:
return Position(self.line - pos.line, self.col)
else:
return Position(self.line - pos.line, pos.col)
return Position(self.line - pos.line, self.col - pos.col)
def __add__(self, pos):
assert isinstance(pos, Position)
return Position(self.line + pos.line, self.col + pos.col)
def __sub__(self, pos):
assert isinstance(pos, Position)
return Position(self.line - pos.line, self.col - pos.col)
def __eq__(self, other):
return (self.line, self.col) == (other.line, other.col)
def __ne__(self, other):
return (self.line, self.col) != (other.line, other.col)
def __lt__(self, other):
return (self.line, self.col) < (other.line, other.col)
def __le__(self, other):
return (self.line, self.col) <= (other.line, other.col)
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

View File

@ -0,0 +1 @@
"""Code related to snippets."""

View File

@ -0,0 +1,4 @@
"""In memory representation of snippet definitions."""
from UltiSnips.snippet.definition.ultisnips import UltiSnipsSnippetDefinition
from UltiSnips.snippet.definition.snipmate import SnipMateSnippetDefinition

View File

@ -0,0 +1,443 @@
#!/usr/bin/env python
# encoding: utf-8
"""Snippet representation after parsing."""
import re
import vim
import textwrap
from UltiSnips import _vim
from UltiSnips.compatibility import as_unicode
from UltiSnips.indent_util import IndentUtil
from UltiSnips.text import escape
from UltiSnips.text_objects import SnippetInstance
from UltiSnips.text_objects._python_code import \
SnippetUtilCursor, SnippetUtilForAction
__WHITESPACE_SPLIT = re.compile(r"\s")
def split_at_whitespace(string):
"""Like string.split(), but keeps empty words as empty words."""
return re.split(__WHITESPACE_SPLIT, string)
def _words_for_line(trigger, before, num_words=None):
"""Gets the final 'num_words' words from 'before'.
If num_words is None, then use the number of words in 'trigger'.
"""
if num_words is None:
num_words = len(split_at_whitespace(trigger))
word_list = split_at_whitespace(before)
if len(word_list) <= num_words:
return before.strip()
else:
before_words = before
for i in range(-1, -(num_words + 1), -1):
left = before_words.rfind(word_list[i])
before_words = before_words[:left]
return before[len(before_words):].strip()
class SnippetDefinition(object):
"""Represents a snippet as parsed from a file."""
_INDENT = re.compile(r"^[ \t]*")
_TABS = re.compile(r"^\t*")
def __init__(self, priority, trigger, value, description,
options, globals, location, context, actions):
self._priority = int(priority)
self._trigger = as_unicode(trigger)
self._value = as_unicode(value)
self._description = as_unicode(description)
self._opts = options
self._matched = ''
self._last_re = None
self._globals = globals
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.
self.matches(self._trigger)
def __repr__(self):
return '_SnippetDefinition(%r,%s,%s,%s)' % (
self._priority, self._trigger, self._description, self._opts)
def _re_match(self, trigger):
"""Test if a the current regex trigger matches `trigger`.
If so, set _last_re and _matched.
"""
for match in re.finditer(self._trigger, trigger):
if match.end() != len(trigger):
continue
else:
self._matched = trigger[match.start():match.end()]
self._last_re = match
return match
return False
def _context_match(self, visual_content):
# skip on empty buffer
if len(vim.current.buffer) == 1 and vim.current.buffer[0] == "":
return
locals = {
'context': None,
'visual_mode': '',
'visual_text': '',
'last_placeholder': None
}
if visual_content:
locals['visual_mode'] = visual_content.mode
locals['visual_text'] = visual_content.text
locals['last_placeholder'] = visual_content.placeholder
return self._eval_code('snip.context = ' + self._context_code,
locals).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'),
code
])
current = vim.current
locals = {
'window': current.window,
'buffer': current.buffer,
'line': current.window.cursor[0]-1,
'column': current.window.cursor[1]-1,
'cursor': SnippetUtilCursor(current.window.cursor),
}
locals.update(additional_locals)
snip = SnippetUtilForAction(locals)
try:
exec(code, {'snip': snip})
except Exception as e:
self._make_debug_exception(e, code)
raise
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 _make_debug_exception(self, e, code=''):
e.snippet_info = textwrap.dedent("""
Defined in: {}
Trigger: {}
Description: {}
Context: {}
Pre-expand: {}
Post-expand: {}
""").format(
self._location,
self._trigger,
self._description,
self._context_code if self._context_code else '<none>',
self._actions['pre_expand'] if 'pre_expand' in self._actions
else '<none>',
self._actions['post_expand'] if 'post_expand' in self._actions
else '<none>',
code,
)
e.snippet_code = code
def has_option(self, opt):
"""Check if the named option is set."""
return opt in self._opts
@property
def description(self):
"""Descriptive text for this snippet."""
return ('(%s) %s' % (self._trigger, self._description)).strip()
@property
def priority(self):
"""The snippets priority, which defines which snippet will be preferred
over others with the same trigger."""
return self._priority
@property
def trigger(self):
"""The trigger text for the snippet."""
return self._trigger
@property
def matched(self):
"""The last text that matched this snippet in match() or
could_match()."""
return self._matched
@property
def location(self):
"""Where this snippet was defined."""
return self._location
@property
def context(self):
"""The matched context."""
return self._context
def matches(self, before, visual_content=None):
"""Returns True if this snippet matches 'before'."""
# If user supplies both "w" and "i", it should perhaps be an
# error, but if permitted it seems that "w" should take precedence
# (since matching at word boundary and within a word == matching at word
# boundary).
self._matched = ''
words = _words_for_line(self._trigger, before)
if 'r' in self._opts:
try:
match = self._re_match(before)
except Exception as e:
self._make_debug_exception(e)
raise
elif 'w' in self._opts:
words_len = len(self._trigger)
words_prefix = words[:-words_len]
words_suffix = words[-words_len:]
match = (words_suffix == self._trigger)
if match and words_prefix:
# Require a word boundary between prefix and suffix.
boundary_chars = escape(words_prefix[-1:] +
words_suffix[:1], r'\"')
match = _vim.eval(
'"%s" =~# "\\\\v.<."' %
boundary_chars) != '0'
elif 'i' in self._opts:
match = words.endswith(self._trigger)
else:
match = (words == self._trigger)
# By default, we match the whole trigger
if match and not self._matched:
self._matched = self._trigger
# Ensure the match was on a word boundry if needed
if 'b' in self._opts and match:
text_before = before.rstrip()[:-len(self._matched)]
if text_before.strip(' \t') != '':
self._matched = ''
return False
self._context = None
if match and self._context_code:
self._context = self._context_match(visual_content)
if not self.context:
match = False
return match
def could_match(self, before):
"""Return True if this snippet could match the (partial) 'before'."""
self._matched = ''
# List all on whitespace.
if before and before[-1] in (' ', '\t'):
before = ''
if before and before.rstrip() is not before:
return False
words = _words_for_line(self._trigger, before)
if 'r' in self._opts:
# Test for full match only
match = self._re_match(before)
elif 'w' in self._opts:
# Trim non-empty prefix up to word boundary, if present.
qwords = escape(words, r'\"')
words_suffix = _vim.eval(
'substitute("%s", "\\\\v^.+<(.+)", "\\\\1", "")' % qwords)
match = self._trigger.startswith(words_suffix)
self._matched = words_suffix
# TODO: list_snippets() function cannot handle partial-trigger
# matches yet, so for now fail if we trimmed the prefix.
if words_suffix != words:
match = False
elif 'i' in self._opts:
# TODO: It is hard to define when a inword snippet could match,
# therefore we check only for full-word trigger.
match = self._trigger.startswith(words)
else:
match = self._trigger.startswith(words)
# By default, we match the words from the trigger
if match and not self._matched:
self._matched = words
# Ensure the match was on a word boundry if needed
if 'b' in self._opts and match:
text_before = before.rstrip()[:-len(self._matched)]
if text_before.strip(' \t') != '':
self._matched = ''
return False
return match
def instantiate(self, snippet_instance, initial_text, indent):
"""Parses the content of this snippet and brings the corresponding text
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.
'Parent' is the parent snippet instance if any.
"""
indent = self._INDENT.match(text_before).group(0)
lines = (self._value + '\n').splitlines()
ind_util = IndentUtil()
# Replace leading tabs in the snippet definition via proper indenting
initial_text = []
for line_num, line in enumerate(lines):
if 't' in self._opts:
tabs = 0
else:
tabs = len(self._TABS.match(line).group(0))
line_ind = ind_util.ntabs_to_proper_indent(tabs)
if line_num != 0:
line_ind = indent + line_ind
result_line = line_ind + line[tabs:]
if 'm' in self._opts:
result_line = result_line.rstrip()
initial_text.append(result_line)
initial_text = '\n'.join(initial_text)
snippet_instance = SnippetInstance(
self, parent, initial_text, start, end, visual_content,
last_re=self._last_re, globals=self._globals,
context=self._context)
self.instantiate(snippet_instance, initial_text, indent)
snippet_instance.update_textobjects()
return snippet_instance

View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
# encoding: utf-8
"""A snipMate snippet after parsing."""
from UltiSnips.snippet.definition._base import SnippetDefinition
from UltiSnips.snippet.parsing.snipmate import parse_and_instantiate
class SnipMateSnippetDefinition(SnippetDefinition):
"""See module doc."""
SNIPMATE_SNIPPET_PRIORITY = -1000
def __init__(self, trigger, value, description, location):
SnippetDefinition.__init__(self, self.SNIPMATE_SNIPPET_PRIORITY,
trigger, value, description, '', {}, location,
None, {})
def instantiate(self, snippet_instance, initial_text, indent):
parse_and_instantiate(snippet_instance, initial_text, indent)

View File

@ -0,0 +1,15 @@
#!/usr/bin/env python
# encoding: utf-8
"""A UltiSnips snippet after parsing."""
from UltiSnips.snippet.definition._base import SnippetDefinition
from UltiSnips.snippet.parsing.ultisnips import parse_and_instantiate
class UltiSnipsSnippetDefinition(SnippetDefinition):
"""See module doc."""
def instantiate(self, snippet_instance, initial_text, indent):
return parse_and_instantiate(snippet_instance, initial_text, indent)

View File

@ -0,0 +1 @@
"""Code related to turning text into snippets."""

View File

@ -0,0 +1,68 @@
#!/usr/bin/env python
# encoding: utf-8
"""Common functionality of the snippet parsing codes."""
from UltiSnips.position import Position
from UltiSnips.snippet.parsing._lexer import tokenize, TabStopToken
from UltiSnips.text_objects import TabStop
from UltiSnips.text_objects import Mirror
from UltiSnips.snippet.parsing._lexer import MirrorToken
def resolve_ambiguity(all_tokens, seen_ts):
"""$1 could be a Mirror or a TabStop.
This figures this out.
"""
for parent, token in all_tokens:
if isinstance(token, MirrorToken):
if token.number not in seen_ts:
seen_ts[token.number] = TabStop(parent, token)
else:
Mirror(parent, seen_ts[token.number], token)
def tokenize_snippet_text(snippet_instance, text, indent,
allowed_tokens_in_text, allowed_tokens_in_tabstops,
token_to_textobject):
"""Turns 'text' into a stream of tokens and creates the text objects from
those tokens that are mentioned in 'token_to_textobject' assuming the
current 'indent'.
The 'allowed_tokens_in_text' define which tokens will be recognized
in 'text' while 'allowed_tokens_in_tabstops' are the tokens that
will be recognized in TabStop placeholder text.
"""
seen_ts = {}
all_tokens = []
def _do_parse(parent, text, allowed_tokens):
"""Recursive function that actually creates the objects."""
tokens = list(tokenize(text, indent, parent.start, allowed_tokens))
for token in tokens:
all_tokens.append((parent, token))
if isinstance(token, TabStopToken):
ts = TabStop(parent, token)
seen_ts[token.number] = ts
_do_parse(ts, token.initial_text,
allowed_tokens_in_tabstops)
else:
klass = token_to_textobject.get(token.__class__, None)
if klass is not None:
klass(parent, token)
_do_parse(snippet_instance, text, allowed_tokens_in_text)
return all_tokens, seen_ts
def finalize(all_tokens, seen_ts, snippet_instance):
"""Adds a tabstop 0 if non is in 'seen_ts' and brings the text of the
snippet instance into Vim."""
if 0 not in seen_ts:
mark = all_tokens[-1][1].end # Last token is always EndOfText
m1 = Position(mark.line, mark.col)
TabStop(snippet_instance, 0, mark, m1)
snippet_instance.replace_initial_text()

View File

@ -0,0 +1,369 @@
#!/usr/bin/env python
# encoding: utf-8
"""Not really a lexer in the classical sense, but code to convert snippet
definitions into logical units called Tokens."""
import string
import re
from UltiSnips.compatibility import as_unicode
from UltiSnips.position import Position
from UltiSnips.text import unescape
class _TextIterator(object):
"""Helper class to make iterating over text easier."""
def __init__(self, text, offset):
self._text = as_unicode(text)
self._line = offset.line
self._col = offset.col
self._idx = 0
def __iter__(self):
"""Iterator interface."""
return self
def __next__(self):
"""Returns the next character."""
if self._idx >= len(self._text):
raise StopIteration
rv = self._text[self._idx]
if self._text[self._idx] in ('\n', '\r\n'):
self._line += 1
self._col = 0
else:
self._col += 1
self._idx += 1
return rv
next = __next__ # for python2
def peek(self, count=1):
"""Returns the next 'count' characters without advancing the stream."""
if count > 1: # This might return '' if nothing is found
return self._text[self._idx:self._idx + count]
try:
return self._text[self._idx]
except IndexError:
return None
@property
def pos(self):
"""Current position in the text."""
return Position(self._line, self._col)
def _parse_number(stream):
"""Expects the stream to contain a number next, returns the number without
consuming any more bytes."""
rv = ''
while stream.peek() and stream.peek() in string.digits:
rv += next(stream)
return int(rv)
def _parse_till_closing_brace(stream):
"""
Returns all chars till a non-escaped } is found. Other
non escaped { are taken into account and skipped over.
Will also consume the closing }, but not return it
"""
rv = ''
in_braces = 1
while True:
if EscapeCharToken.starts_here(stream, '{}'):
rv += next(stream) + next(stream)
else:
char = next(stream)
if char == '{':
in_braces += 1
elif char == '}':
in_braces -= 1
if in_braces == 0:
break
rv += char
return rv
def _parse_till_unescaped_char(stream, chars):
"""
Returns all chars till a non-escaped char is found.
Will also consume the closing char, but and return it as second
return value
"""
rv = ''
while True:
escaped = False
for char in chars:
if EscapeCharToken.starts_here(stream, char):
rv += next(stream) + next(stream)
escaped = True
if not escaped:
char = next(stream)
if char in chars:
break
rv += char
return rv, char
class Token(object):
"""Represents a Token as parsed from a snippet definition."""
def __init__(self, gen, indent):
self.initial_text = as_unicode('')
self.start = gen.pos
self._parse(gen, indent)
self.end = gen.pos
def _parse(self, stream, indent):
"""Parses the token from 'stream' with the current 'indent'."""
pass # Does nothing
class TabStopToken(Token):
"""${1:blub}"""
CHECK = re.compile(r'^\${\d+[:}]')
@classmethod
def starts_here(cls, stream):
"""Returns true if this token starts at the current position in
'stream'."""
return cls.CHECK.match(stream.peek(10)) is not None
def _parse(self, stream, indent):
next(stream) # $
next(stream) # {
self.number = _parse_number(stream)
if stream.peek() == ':':
next(stream)
self.initial_text = _parse_till_closing_brace(stream)
def __repr__(self):
return 'TabStopToken(%r,%r,%r,%r)' % (
self.start, self.end, self.number, self.initial_text
)
class VisualToken(Token):
"""${VISUAL}"""
CHECK = re.compile(r"^\${VISUAL[:}/]")
@classmethod
def starts_here(cls, stream):
"""Returns true if this token starts at the current position in
'stream'."""
return cls.CHECK.match(stream.peek(10)) is not None
def _parse(self, stream, indent):
for _ in range(8): # ${VISUAL
next(stream)
if stream.peek() == ':':
next(stream)
self.alternative_text, char = _parse_till_unescaped_char(stream, '/}')
self.alternative_text = unescape(self.alternative_text)
if char == '/': # Transformation going on
try:
self.search = _parse_till_unescaped_char(stream, '/')[0]
self.replace = _parse_till_unescaped_char(stream, '/')[0]
self.options = _parse_till_closing_brace(stream)
except StopIteration:
raise RuntimeError(
"Invalid ${VISUAL} transformation! Forgot to escape a '/'?")
else:
self.search = None
self.replace = None
self.options = None
def __repr__(self):
return 'VisualToken(%r,%r)' % (
self.start, self.end
)
class TransformationToken(Token):
"""${1/match/replace/options}"""
CHECK = re.compile(r'^\${\d+\/')
@classmethod
def starts_here(cls, stream):
"""Returns true if this token starts at the current position in
'stream'."""
return cls.CHECK.match(stream.peek(10)) is not None
def _parse(self, stream, indent):
next(stream) # $
next(stream) # {
self.number = _parse_number(stream)
next(stream) # /
self.search = _parse_till_unescaped_char(stream, '/')[0]
self.replace = _parse_till_unescaped_char(stream, '/')[0]
self.options = _parse_till_closing_brace(stream)
def __repr__(self):
return 'TransformationToken(%r,%r,%r,%r,%r)' % (
self.start, self.end, self.number, self.search, self.replace
)
class MirrorToken(Token):
"""$1."""
CHECK = re.compile(r'^\$\d+')
@classmethod
def starts_here(cls, stream):
"""Returns true if this token starts at the current position in
'stream'."""
return cls.CHECK.match(stream.peek(10)) is not None
def _parse(self, stream, indent):
next(stream) # $
self.number = _parse_number(stream)
def __repr__(self):
return 'MirrorToken(%r,%r,%r)' % (
self.start, self.end, self.number
)
class EscapeCharToken(Token):
"""\\n."""
@classmethod
def starts_here(cls, stream, chars=r'{}\$`'):
"""Returns true if this token starts at the current position in
'stream'."""
cs = stream.peek(2)
if len(cs) == 2 and cs[0] == '\\' and cs[1] in chars:
return True
def _parse(self, stream, indent):
next(stream) # \
self.initial_text = next(stream)
def __repr__(self):
return 'EscapeCharToken(%r,%r,%r)' % (
self.start, self.end, self.initial_text
)
class ShellCodeToken(Token):
"""`echo "hi"`"""
@classmethod
def starts_here(cls, stream):
"""Returns true if this token starts at the current position in
'stream'."""
return stream.peek(1) == '`'
def _parse(self, stream, indent):
next(stream) # `
self.code = _parse_till_unescaped_char(stream, '`')[0]
def __repr__(self):
return 'ShellCodeToken(%r,%r,%r)' % (
self.start, self.end, self.code
)
class PythonCodeToken(Token):
"""`!p snip.rv = "Hi"`"""
CHECK = re.compile(r'^`!p\s')
@classmethod
def starts_here(cls, stream):
"""Returns true if this token starts at the current position in
'stream'."""
return cls.CHECK.match(stream.peek(4)) is not None
def _parse(self, stream, indent):
for _ in range(3):
next(stream) # `!p
if stream.peek() in '\t ':
next(stream)
code = _parse_till_unescaped_char(stream, '`')[0]
# Strip the indent if any
if len(indent):
lines = code.splitlines()
self.code = lines[0] + '\n'
self.code += '\n'.join([l[len(indent):]
for l in lines[1:]])
else:
self.code = code
self.indent = indent
def __repr__(self):
return 'PythonCodeToken(%r,%r,%r)' % (
self.start, self.end, self.code
)
class VimLCodeToken(Token):
"""`!v g:hi`"""
CHECK = re.compile(r'^`!v\s')
@classmethod
def starts_here(cls, stream):
"""Returns true if this token starts at the current position in
'stream'."""
return cls.CHECK.match(stream.peek(4)) is not None
def _parse(self, stream, indent):
for _ in range(4):
next(stream) # `!v
self.code = _parse_till_unescaped_char(stream, '`')[0]
def __repr__(self):
return 'VimLCodeToken(%r,%r,%r)' % (
self.start, self.end, self.code
)
class EndOfTextToken(Token):
"""Appears at the end of the text."""
def __repr__(self):
return 'EndOfText(%r)' % self.end
def tokenize(text, indent, offset, allowed_tokens):
"""Returns an iterator of tokens of 'text'['offset':] which is assumed to
have 'indent' as the whitespace of the begging of the lines. Only
'allowed_tokens' are considered to be valid tokens."""
stream = _TextIterator(text, offset)
try:
while True:
done_something = False
for token in allowed_tokens:
if token.starts_here(stream):
yield token(stream, indent)
done_something = True
break
if not done_something:
next(stream)
except StopIteration:
yield EndOfTextToken(stream, indent)

View File

@ -0,0 +1,38 @@
#!/usr/bin/env python
# encoding: utf-8
"""Parses a snipMate snippet definition and launches it into Vim."""
from UltiSnips.snippet.parsing._base import tokenize_snippet_text, finalize, resolve_ambiguity
from UltiSnips.snippet.parsing._lexer import EscapeCharToken, \
VisualToken, TabStopToken, MirrorToken, ShellCodeToken
from UltiSnips.text_objects import EscapedChar, Mirror, VimLCode, Visual
_TOKEN_TO_TEXTOBJECT = {
EscapeCharToken: EscapedChar,
VisualToken: Visual,
ShellCodeToken: VimLCode, # `` is VimL in snipMate
}
__ALLOWED_TOKENS = [
EscapeCharToken, VisualToken, TabStopToken, MirrorToken, ShellCodeToken
]
__ALLOWED_TOKENS_IN_TABSTOPS = [
EscapeCharToken, VisualToken, MirrorToken, ShellCodeToken
]
def parse_and_instantiate(parent_to, text, indent):
"""Parses a snippet definition in snipMate format from 'text' assuming the
current 'indent'.
Will instantiate all the objects and link them as children to
parent_to. Will also put the initial text into Vim.
"""
all_tokens, seen_ts = tokenize_snippet_text(parent_to, text, indent,
__ALLOWED_TOKENS, __ALLOWED_TOKENS_IN_TABSTOPS,
_TOKEN_TO_TEXTOBJECT)
resolve_ambiguity(all_tokens, seen_ts)
finalize(all_tokens, seen_ts, parent_to)

View File

@ -0,0 +1,50 @@
#!/usr/bin/env python
# encoding: utf-8
"""Parses a UltiSnips snippet definition and launches it into Vim."""
from UltiSnips.snippet.parsing._base import tokenize_snippet_text, finalize, resolve_ambiguity
from UltiSnips.snippet.parsing._lexer import EscapeCharToken, \
VisualToken, TransformationToken, TabStopToken, MirrorToken, \
PythonCodeToken, VimLCodeToken, ShellCodeToken
from UltiSnips.text_objects import EscapedChar, Mirror, PythonCode, \
ShellCode, TabStop, Transformation, VimLCode, Visual
_TOKEN_TO_TEXTOBJECT = {
EscapeCharToken: EscapedChar,
VisualToken: Visual,
ShellCodeToken: ShellCode,
PythonCodeToken: PythonCode,
VimLCodeToken: VimLCode,
}
__ALLOWED_TOKENS = [
EscapeCharToken, VisualToken, TransformationToken, TabStopToken,
MirrorToken, PythonCodeToken, VimLCodeToken, ShellCodeToken
]
def _create_transformations(all_tokens, seen_ts):
"""Create the objects that need to know about tabstops."""
for parent, token in all_tokens:
if isinstance(token, TransformationToken):
if token.number not in seen_ts:
raise RuntimeError(
'Tabstop %i is not known but is used by a Transformation'
% token.number)
Transformation(parent, seen_ts[token.number], token)
def parse_and_instantiate(parent_to, text, indent):
"""Parses a snippet definition in UltiSnips format from 'text' assuming the
current 'indent'.
Will instantiate all the objects and link them as children to
parent_to. Will also put the initial text into Vim.
"""
all_tokens, seen_ts = tokenize_snippet_text(parent_to, text, indent,
__ALLOWED_TOKENS, __ALLOWED_TOKENS, _TOKEN_TO_TEXTOBJECT)
resolve_ambiguity(all_tokens, seen_ts)
_create_transformations(all_tokens, seen_ts)
finalize(all_tokens, seen_ts, parent_to)

View File

@ -0,0 +1,10 @@
#!/usr/bin/env python
# encoding: utf-8
"""Sources of snippet definitions."""
from UltiSnips.snippet.source._base import SnippetSource
from UltiSnips.snippet.source.added import AddedSnippetsSource
from UltiSnips.snippet.source.file.snipmate import SnipMateFileSource
from UltiSnips.snippet.source.file.ultisnips import UltiSnipsFileSource, \
find_all_snippet_files, find_snippet_files

View File

@ -0,0 +1,97 @@
#!/usr/bin/env python
# encoding: utf-8
"""Base class for snippet sources."""
from collections import defaultdict
from UltiSnips.snippet.source._snippet_dictionary import SnippetDictionary
class SnippetSource(object):
"""See module docstring."""
def __init__(self):
self._snippets = defaultdict(SnippetDictionary)
self._extends = defaultdict(set)
def ensure(self, filetypes, cached):
"""Update/reload the snippets in the source when needed.
It makes sure that the snippets are not outdated.
"""
def loaded(self, filetypes):
return len(self._snippets) > 0
def _get_existing_deep_extends(self, base_filetypes):
"""Helper for get all existing filetypes extended by base filetypes."""
deep_extends = self.get_deep_extends(base_filetypes)
return [ft for ft in deep_extends if ft in self._snippets]
def get_snippets(self, filetypes, before, possible, autotrigger_only,
visual_content):
"""Returns the snippets for all 'filetypes' (in order) and their
parents matching the text 'before'. If 'possible' is true, a partial
match is enough. Base classes can override this method to provide means
of creating snippets on the fly.
Returns a list of SnippetDefinition s.
"""
result = []
for ft in self._get_existing_deep_extends(filetypes):
snips = self._snippets[ft]
result.extend(snips.get_matching_snippets(before, possible,
autotrigger_only,
visual_content))
return result
def get_clear_priority(self, filetypes):
"""Get maximum clearsnippets priority without arguments for specified
filetypes, if any.
It returns None if there are no clearsnippets.
"""
pri = None
for ft in self._get_existing_deep_extends(filetypes):
snippets = self._snippets[ft]
if pri is None or snippets._clear_priority > pri:
pri = snippets._clear_priority
return pri
def get_cleared(self, filetypes):
"""Get a set of cleared snippets marked by clearsnippets with arguments
for specified filetypes."""
cleared = {}
for ft in self._get_existing_deep_extends(filetypes):
snippets = self._snippets[ft]
for key, value in snippets._cleared.items():
if key not in cleared or value > cleared[key]:
cleared[key] = value
return cleared
def update_extends(self, child_ft, parent_fts):
"""Update the extending relation by given child filetype and its parent
filetypes."""
self._extends[child_ft].update(parent_fts)
def get_deep_extends(self, base_filetypes):
"""Get a list of filetypes that is either directed or indirected
extended by given base filetypes.
Note that the returned list include the root filetype itself.
"""
seen = set(base_filetypes)
todo_fts = list(set(base_filetypes))
while todo_fts:
todo_ft = todo_fts.pop()
unseen_extends = set(
ft for ft in self._extends[todo_ft] if ft not in seen)
seen.update(unseen_extends)
todo_fts.extend(unseen_extends)
return seen

View File

@ -0,0 +1,60 @@
#!/usr/bin/env python
# encoding: utf-8
"""Implements a container for parsed snippets."""
class SnippetDictionary(object):
"""See module docstring."""
def __init__(self):
self._snippets = []
self._cleared = {}
self._clear_priority = float("-inf")
def add_snippet(self, snippet):
"""Add 'snippet' to this dictionary."""
self._snippets.append(snippet)
def get_matching_snippets(self, trigger, potentially, autotrigger_only,
visual_content):
"""Returns all snippets matching the given trigger.
If 'potentially' is true, returns all that could_match().
If 'autotrigger_only' is true, function will return only snippets which
are marked with flag 'A' (should be automatically expanded without
trigger key press).
It's handled specially to avoid walking down the list of all snippets,
which can be very slow, because function will be called on each change
made in insert mode.
"""
all_snippets = self._snippets
if autotrigger_only:
all_snippets = [s for s in all_snippets if s.has_option('A')]
if not potentially:
return [s for s in all_snippets if s.matches(trigger,
visual_content)]
else:
return [s for s in all_snippets if s.could_match(trigger)]
def clear_snippets(self, priority, triggers):
"""Clear the snippets by mark them as cleared.
If trigger is None, it updates the value of clear priority
instead.
"""
if not triggers:
if self._clear_priority is None or priority > self._clear_priority:
self._clear_priority = priority
else:
for trigger in triggers:
if (trigger not in self._cleared or
priority > self._cleared[trigger]):
self._cleared[trigger] = priority
def __len__(self):
return len(self._snippets)

View File

@ -0,0 +1,15 @@
#!/usr/bin/env python
# encoding: utf-8
"""Handles manually added snippets UltiSnips_Manager.add_snippet()."""
from UltiSnips.snippet.source._base import SnippetSource
class AddedSnippetsSource(SnippetSource):
"""See module docstring."""
def add_snippet(self, ft, snippet):
"""Adds the given 'snippet' for 'ft'."""
self._snippets[ft].add_snippet(snippet)

View File

@ -0,0 +1 @@
"""Snippet sources that are file based."""

View File

@ -0,0 +1,112 @@
#!/usr/bin/env python
# encoding: utf-8
"""Code to provide access to UltiSnips files from disk."""
from collections import defaultdict
import hashlib
import os
from UltiSnips import _vim
from UltiSnips import compatibility
from UltiSnips.snippet.source._base import SnippetSource
def _hash_file(path):
"""Returns a hashdigest of 'path'."""
if not os.path.isfile(path):
return False
return hashlib.sha1(open(path, 'rb').read()).hexdigest()
class SnippetSyntaxError(RuntimeError):
"""Thrown when a syntax error is found in a file."""
def __init__(self, filename, line_index, msg):
RuntimeError.__init__(self, '%s in %s:%d' % (
msg, filename, line_index))
class SnippetFileSource(SnippetSource):
"""Base class that abstracts away 'extends' info and file hashes."""
def __init__(self):
SnippetSource.__init__(self)
self._files_for_ft = defaultdict(set)
self._file_hashes = defaultdict(lambda: None)
self._ensure_cached = False
def ensure(self, filetypes, cached):
if cached and self._ensure_cached:
return
for ft in self.get_deep_extends(filetypes):
if self._needs_update(ft):
self._load_snippets_for(ft)
self._ensure_cached = True
def _get_all_snippet_files_for(self, ft):
"""Returns a set of all files that define snippets for 'ft'."""
raise NotImplementedError()
def _parse_snippet_file(self, filedata, filename):
"""Parses 'filedata' as a snippet file and yields events."""
raise NotImplementedError()
def _needs_update(self, ft):
"""Returns true if any files for 'ft' have changed and must be
reloaded."""
existing_files = self._get_all_snippet_files_for(ft)
if existing_files != self._files_for_ft[ft]:
self._files_for_ft[ft] = existing_files
return True
for filename in self._files_for_ft[ft]:
if _hash_file(filename) != self._file_hashes[filename]:
return True
return False
def _load_snippets_for(self, ft):
"""Load all snippets for the given 'ft'."""
if ft in self._snippets:
del self._snippets[ft]
del self._extends[ft]
try:
for fn in self._files_for_ft[ft]:
self._parse_snippets(ft, fn)
except:
del self._files_for_ft[ft]
raise
# Now load for the parents
for parent_ft in self.get_deep_extends([ft]):
if parent_ft != ft and self._needs_update(parent_ft):
self._load_snippets_for(parent_ft)
def _parse_snippets(self, ft, filename):
"""Parse the 'filename' for the given 'ft' and watch it for changes in
the future."""
self._file_hashes[filename] = _hash_file(filename)
file_data = compatibility.open_ascii_file(filename, 'r').read()
for event, data in self._parse_snippet_file(file_data, filename):
if event == 'error':
msg, line_index = data
filename = _vim.eval("""fnamemodify(%s, ":~:.")""" %
_vim.escape(filename))
raise SnippetSyntaxError(filename, line_index, msg)
elif event == 'clearsnippets':
priority, triggers = data
self._snippets[ft].clear_snippets(priority, triggers)
elif event == 'extends':
# TODO(sirver): extends information is more global
# than one snippet source.
filetypes, = data
self.update_extends(ft, filetypes)
elif event == 'snippet':
snippet, = data
self._snippets[ft].add_snippet(snippet)
else:
assert False, 'Unhandled %s: %r' % (event, data)

View File

@ -0,0 +1,29 @@
#!/usr/bin/env python
# encoding: utf-8
"""Common code for snipMate and UltiSnips snippet files."""
def handle_extends(tail, line_index):
"""Handles an extends line in a snippet."""
if tail:
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)
def handle_context(tail, line_index):
if tail:
return 'context', tail.strip('"').replace(r'\"', '"')\
.replace(r'\\\\', r'\\')
else:
return 'error', ("'context' without body", line_index)

View File

@ -0,0 +1,127 @@
#!/usr/bin/env python
# encoding: utf-8
"""Parses snipMate files."""
import os
import glob
from UltiSnips import _vim
from UltiSnips.snippet.definition import SnipMateSnippetDefinition
from UltiSnips.snippet.source.file._base import SnippetFileSource
from UltiSnips.snippet.source.file._common import handle_extends
from UltiSnips.text import LineIterator, head_tail
def _splitall(path):
"""Split 'path' into all its components."""
# From http://my.safaribooksonline.com/book/programming/
# python/0596001673/files/pythoncook-chp-4-sect-16
allparts = []
while True:
parts = os.path.split(path)
if parts[0] == path: # sentinel for absolute paths
allparts.insert(0, parts[0])
break
elif parts[1] == path: # sentinel for relative paths
allparts.insert(0, parts[1])
break
else:
path = parts[0]
allparts.insert(0, parts[1])
return allparts
def snipmate_files_for(ft):
"""Returns all snipMate files we need to look at for 'ft'."""
if ft == 'all':
ft = '_'
patterns = [
'%s.snippets' % ft,
os.path.join(ft, '*.snippets'),
os.path.join(ft, '*.snippet'),
os.path.join(ft, '*/*.snippet'),
]
ret = set()
for rtp in _vim.eval('&runtimepath').split(','):
path = os.path.realpath(os.path.expanduser(
os.path.join(rtp, 'snippets')))
for pattern in patterns:
for fn in glob.glob(os.path.join(path, pattern)):
ret.add(fn)
return ret
def _parse_snippet_file(content, full_filename):
"""Parses 'content' assuming it is a .snippet file and yields events."""
filename = full_filename[:-len('.snippet')] # strip extension
segments = _splitall(filename)
segments = segments[segments.index('snippets') + 1:]
assert len(segments) in (2, 3)
trigger = segments[1]
description = segments[2] if 2 < len(segments) else ''
# Chomp \n if any.
if content and content.endswith(os.linesep):
content = content[:-len(os.linesep)]
yield 'snippet', (SnipMateSnippetDefinition(trigger, content,
description, full_filename),)
def _parse_snippet(line, lines, filename):
"""Parse a snippet defintions."""
start_line_index = lines.line_index
trigger, description = head_tail(line[len('snippet'):].lstrip())
content = ''
while True:
next_line = lines.peek()
if next_line is None:
break
if next_line.strip() and not next_line.startswith('\t'):
break
line = next(lines)
if line[0] == '\t':
line = line[1:]
content += line
content = content[:-1] # Chomp the last newline
return 'snippet', (SnipMateSnippetDefinition(
trigger, content, description, '%s:%i' % (filename, start_line_index)),)
def _parse_snippets_file(data, filename):
"""Parse 'data' assuming it is a .snippets file.
Yields events in the file.
"""
lines = LineIterator(data)
for line in lines:
if not line.strip():
continue
head, tail = head_tail(line)
if head == 'extends':
yield handle_extends(tail, lines.line_index)
elif head in 'snippet':
snippet = _parse_snippet(line, lines, filename)
if snippet is not None:
yield snippet
elif head and not head.startswith('#'):
yield 'error', ('Invalid line %r' % line.rstrip(), lines.line_index)
class SnipMateFileSource(SnippetFileSource):
"""Manages all snipMate snippet definitions found in rtp."""
def _get_all_snippet_files_for(self, ft):
return snipmate_files_for(ft)
def _parse_snippet_file(self, filedata, filename):
if filename.lower().endswith('snippet'):
for event, data in _parse_snippet_file(filedata, filename):
yield event, data
else:
for event, data in _parse_snippets_file(filedata, filename):
yield event, data

View File

@ -0,0 +1,187 @@
#!/usr/bin/env python
# encoding: utf-8
"""Parsing of snippet files."""
from collections import defaultdict
import glob
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, \
handle_action, handle_context
from UltiSnips.text import LineIterator, head_tail
def find_snippet_files(ft, directory):
"""Returns all matching snippet files for 'ft' in 'directory'."""
patterns = ['%s.snippets', '%s_*.snippets', os.path.join('%s', '*')]
ret = set()
directory = os.path.expanduser(directory)
for pattern in patterns:
for fn in glob.glob(os.path.join(directory, pattern % ft)):
ret.add(os.path.realpath(fn))
return ret
def find_all_snippet_files(ft):
"""Returns all snippet files matching 'ft' in the given runtime path
directory."""
if _vim.eval("exists('b:UltiSnipsSnippetDirectories')") == '1':
snippet_dirs = _vim.eval('b:UltiSnipsSnippetDirectories')
else:
snippet_dirs = _vim.eval('g:UltiSnipsSnippetDirectories')
if len(snippet_dirs) == 1 and os.path.isabs(snippet_dirs[0]):
check_dirs = ['']
else:
check_dirs = _vim.eval('&runtimepath').split(',')
patterns = ['%s.snippets', '%s_*.snippets', os.path.join('%s', '*')]
ret = set()
for rtp in check_dirs:
for snippet_dir in snippet_dirs:
if snippet_dir == 'snippets':
raise RuntimeError(
"You have 'snippets' in UltiSnipsSnippetDirectories. This "
'directory is reserved for snipMate snippets. Use another '
'directory for UltiSnips snippets.')
pth = os.path.realpath(os.path.expanduser(
os.path.join(rtp, snippet_dir)))
for pattern in patterns:
for fn in glob.glob(os.path.join(pth, pattern % ft)):
ret.add(fn)
return ret
def _handle_snippet_or_global(
filename, line, lines, python_globals, priority, pre_expand, context
):
"""Parses the snippet that begins at the current line."""
start_line_index = lines.line_index
descr = ''
opts = ''
# Ensure this is a snippet
snip = line.split()[0]
# Get and strip options if they exist
remain = line[len(snip):].strip()
words = remain.split()
if len(words) > 2:
# second to last word ends with a quote
if '"' not in words[-1] and words[-2][-1] == '"':
opts = words[-1]
remain = remain[:-len(opts) - 1].rstrip()
if 'e' in opts and not context:
left = remain[:-1].rfind('"')
if left != -1 and left != 0:
context, remain = remain[left:].strip('"'), remain[:left]
# Get and strip description if it exists
remain = remain.strip()
if len(remain.split()) > 1 and remain[-1] == '"':
left = remain[:-1].rfind('"')
if left != -1 and left != 0:
descr, remain = remain[left:], remain[:left]
# The rest is the trigger
trig = remain.strip()
if len(trig.split()) > 1 or 'r' in opts:
if trig[0] != trig[-1]:
return 'error', ("Invalid multiword trigger: '%s'" % trig,
lines.line_index)
trig = trig[1:-1]
end = 'end' + snip
content = ''
found_end = False
for line in lines:
if line.rstrip() == end:
content = content[:-1] # Chomp the last newline
found_end = True
break
content += line
if not found_end:
return 'error', ("Missing 'endsnippet' for %r" %
trig, lines.line_index)
if snip == 'global':
python_globals[trig].append(content)
elif snip == 'snippet':
definition = UltiSnipsSnippetDefinition(
priority, trig, content,
descr, opts, python_globals,
'%s:%i' % (filename, start_line_index),
context, pre_expand)
return 'snippet', (definition,)
else:
return 'error', ("Invalid snippet type: '%s'" % snip, lines.line_index)
def _parse_snippets_file(data, filename):
"""Parse 'data' assuming it is a snippet file.
Yields events in the file.
"""
python_globals = defaultdict(list)
lines = LineIterator(data)
current_priority = 0
actions = {}
context = None
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,
actions,
context
)
actions = {}
context = None
if snippet is not None:
yield snippet
elif head == 'extends':
yield handle_extends(tail, lines.line_index)
elif head == 'clearsnippets':
yield 'clearsnippets', (current_priority, tail.split())
elif head == 'context':
head, context, = handle_context(tail, lines.line_index)
if head == 'error':
yield (head, tail)
elif head == 'priority':
try:
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)
class UltiSnipsFileSource(SnippetFileSource):
"""Manages all snippets definitions found in rtp for ultisnips."""
def _get_all_snippet_files_for(self, ft):
return find_all_snippet_files(ft)
def _parse_snippet_file(self, filedata, filename):
for event, data in _parse_snippets_file(filedata, filename):
yield event, data

View File

@ -0,0 +1,869 @@
#!/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 import err_to_scratch_buffer
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)
# 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._added_buffer_filetypes = defaultdict(lambda: [])
self._vstate = VimState()
self._visual_content = VisualContentPreserver()
self._snippet_sources = []
self._snip_expanded_in_action = False
self._inside_action = False
self._last_change = ('', 0)
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._should_update_textobjects = False
self._should_reset_visual = False
self._reinit()
@err_to_scratch_buffer.wrap
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.wrap
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.wrap
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.wrap
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.wrap
def snippets_in_current_scope(self, searchAll):
"""Returns the snippets that could be expanded to Vim as a global
variable."""
before = '' if searchAll else _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:]
location = snip.location if snip.location else ''
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("'", "''")))
if searchAll:
_vim.command(as_unicode(
("let g:current_ulti_dict_info['{key}'] = {{"
"'description': '{description}',"
"'location': '{location}',"
"}}")).format(
key=key.replace("'", "''"),
location=location.replace("'", "''"),
description=description.replace("'", "''")))
@err_to_scratch_buffer.wrap
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.wrap
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.wrap
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._visual_content):
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 get_buffer_filetypes(self):
return (self._added_buffer_filetypes[_vim.buf.number] +
_vim.buf.filetypes + ['all'])
def add_buffer_filetypes(self, ft):
buf_fts = self._added_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._added_buffer_filetypes[_vim.buf.number].insert(idx + 1, ft)
idx += 1
@err_to_scratch_buffer.wrap
def _cursor_moved(self):
"""Called whenever the cursor moved."""
self._should_update_textobjects = False
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.wrap
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):
"""Helper method that does the actual jump."""
if self._should_update_textobjects:
self._should_reset_visual = False
self._cursor_moved()
# 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'):
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
self._ctab = ntab
# Run interpolations again to update new placeholder
# values, binded to currently newly jumped placeholder.
self._visual_content.conserve_placeholder(self._ctab)
self._cs.current_placeholder = \
self._visual_content.placeholder
self._should_reset_visual = False
self._csnippets[0].update_textobjects()
self._vstate.remember_buffer(self._csnippets[0])
if ntab.number == 0 and self._csnippets:
self._current_snippet_is_done()
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:
if self._ctab:
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.get_buffer_filetypes()[::-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,
self._visual_content
)
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
self._should_update_textobjects = 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):
"""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.
"""
snippet_dir = ''
if _vim.eval("exists('g:UltiSnipsSnippetsDir')") == '1':
dir = _vim.eval('g:UltiSnipsSnippetsDir')
file = self._get_file_to_edit(dir, requested_ft, bang)
if file:
return file
snippet_dir = dir
if _vim.eval("exists('g:UltiSnipsSnippetDirectories')") == '1':
dirs = _vim.eval('g:UltiSnipsSnippetDirectories')
for dir in dirs:
file = self._get_file_to_edit(dir, requested_ft, bang)
if file:
return file
if not snippet_dir:
snippet_dir = dir
home = _vim.eval('$HOME')
if platform.system() == 'Windows':
dir = os.path.join(home, 'vimfiles', 'UltiSnips')
file = self._get_file_to_edit(dir, requested_ft, bang)
if file:
return file
if not snippet_dir:
snippet_dir = dir
if _vim.eval("has('nvim')") == '1':
xdg_home_config = _vim.eval('$XDG_CONFIG_HOME') or os.path.join(home, ".config")
dir = os.path.join(xdg_home_config, 'nvim', 'UltiSnips')
file = self._get_file_to_edit(dir, requested_ft, bang)
if file:
return file
if not snippet_dir:
snippet_dir = dir
dir = os.path.join(home, '.vim', 'UltiSnips')
file = self._get_file_to_edit(dir, requested_ft, bang)
if file:
return file
if not snippet_dir:
snippet_dir = dir
return self._get_file_to_edit(
snippet_dir, requested_ft, bang, True
)
def _get_file_to_edit(self, snippet_dir, requested_ft, bang,
allow_empty=False): # pylint: disable=no-self-use
potentials = set()
filetypes = []
if requested_ft:
filetypes.append(requested_ft)
else:
if bang:
filetypes.extend(self.get_buffer_filetypes())
else:
filetypes.append(self.get_buffer_filetypes()[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()
if not allow_empty and not os.path.exists(file_to_edit):
return ''
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.wrap
def _track_change(self):
self._should_update_textobjects = True
try:
inserted_char = _vim.as_unicode(_vim.eval('v:char'))
except UnicodeDecodeError:
return
if sys.version_info >= (3, 0):
if isinstance(inserted_char, bytes):
return
else:
if not isinstance(inserted_char, unicode):
return
try:
if inserted_char == '':
before = _vim.buf.line_till_cursor
if before and before[-1] == self._last_change[0] or \
self._last_change[1] != vim.current.window.cursor[0]:
self._try_expand(autotrigger_only=True)
finally:
self._last_change = (inserted_char, vim.current.window.cursor[0])
if self._should_reset_visual and self._visual_content.mode == '':
self._visual_content.reset()
self._should_reset_visual = True
UltiSnips_Manager = SnippetManager( # pylint:disable=invalid-name
vim.eval('g:UltiSnipsExpandTrigger'),
vim.eval('g:UltiSnipsJumpForwardTrigger'),
vim.eval('g:UltiSnipsJumpBackwardTrigger'))

View File

@ -0,0 +1,212 @@
#!/usr/bin/env python
# encoding: utf-8
# pylint: skip-file
import unittest
from _diff import diff, guess_edit
from position import Position
def transform(a, cmds):
buf = a.split('\n')
for cmd in cmds:
ctype, line, col, char = cmd
if ctype == 'D':
if char != '\n':
buf[line] = buf[line][:col] + buf[line][col + len(char):]
else:
buf[line] = buf[line] + buf[line + 1]
del buf[line + 1]
elif ctype == 'I':
buf[line] = buf[line][:col] + char + buf[line][col:]
buf = '\n'.join(buf).split('\n')
return '\n'.join(buf)
import unittest
# Test Guessing {{{
class _BaseGuessing(object):
def runTest(self):
rv, es = guess_edit(
self.initial_line, self.a, self.b, Position(*self.ppos), Position(*self.pos))
self.assertEqual(rv, True)
self.assertEqual(self.wanted, es)
class TestGuessing_Noop0(_BaseGuessing, unittest.TestCase):
a, b = [], []
initial_line = 0
ppos, pos = (0, 6), (0, 7)
wanted = ()
class TestGuessing_InsertOneChar(_BaseGuessing, unittest.TestCase):
a, b = ['Hello World'], ['Hello World']
initial_line = 0
ppos, pos = (0, 6), (0, 7)
wanted = (
('I', 0, 6, ' '),
)
class TestGuessing_InsertOneChar1(_BaseGuessing, unittest.TestCase):
a, b = ['Hello World'], ['Hello World']
initial_line = 0
ppos, pos = (0, 7), (0, 8)
wanted = (
('I', 0, 7, ' '),
)
class TestGuessing_BackspaceOneChar(_BaseGuessing, unittest.TestCase):
a, b = ['Hello World'], ['Hello World']
initial_line = 0
ppos, pos = (0, 7), (0, 6)
wanted = (
('D', 0, 6, ' '),
)
class TestGuessing_DeleteOneChar(_BaseGuessing, unittest.TestCase):
a, b = ['Hello World'], ['Hello World']
initial_line = 0
ppos, pos = (0, 5), (0, 5)
wanted = (
('D', 0, 5, ' '),
)
# End: Test Guessing }}}
class _Base(object):
def runTest(self):
es = diff(self.a, self.b)
tr = transform(self.a, es)
self.assertEqual(self.b, tr)
self.assertEqual(self.wanted, es)
class TestEmptyString(_Base, unittest.TestCase):
a, b = '', ''
wanted = ()
class TestAllMatch(_Base, unittest.TestCase):
a, b = 'abcdef', 'abcdef'
wanted = ()
class TestLotsaNewlines(_Base, unittest.TestCase):
a, b = 'Hello', 'Hello\nWorld\nWorld\nWorld'
wanted = (
('I', 0, 5, '\n'),
('I', 1, 0, 'World'),
('I', 1, 5, '\n'),
('I', 2, 0, 'World'),
('I', 2, 5, '\n'),
('I', 3, 0, 'World'),
)
class TestCrash(_Base, unittest.TestCase):
a = 'hallo Blah mitte=sdfdsfsd\nhallo kjsdhfjksdhfkjhsdfkh mittekjshdkfhkhsdfdsf'
b = 'hallo Blah mitte=sdfdsfsd\nhallo b mittekjshdkfhkhsdfdsf'
wanted = (
('D', 1, 6, 'kjsdhfjksdhfkjhsdfkh'),
('I', 1, 6, 'b'),
)
class TestRealLife(_Base, unittest.TestCase):
a = 'hallo End Beginning'
b = 'hallo End t'
wanted = (
('D', 0, 10, 'Beginning'),
('I', 0, 10, 't'),
)
class TestRealLife1(_Base, unittest.TestCase):
a = 'Vorne hallo Hinten'
b = 'Vorne hallo Hinten'
wanted = (
('I', 0, 11, ' '),
)
class TestWithNewline(_Base, unittest.TestCase):
a = 'First Line\nSecond Line'
b = 'n'
wanted = (
('D', 0, 0, 'First Line'),
('D', 0, 0, '\n'),
('D', 0, 0, 'Second Line'),
('I', 0, 0, 'n'),
)
class TestCheapDelete(_Base, unittest.TestCase):
a = 'Vorne hallo Hinten'
b = 'Vorne Hinten'
wanted = (
('D', 0, 5, ' hallo'),
)
class TestNoSubstring(_Base, unittest.TestCase):
a, b = 'abc', 'def'
wanted = (
('D', 0, 0, 'abc'),
('I', 0, 0, 'def'),
)
class TestCommonCharacters(_Base, unittest.TestCase):
a, b = 'hasomelongertextbl', 'hol'
wanted = (
('D', 0, 1, 'asomelongertextb'),
('I', 0, 1, 'o'),
)
class TestUltiSnipsProblem(_Base, unittest.TestCase):
a = 'this is it this is it this is it'
b = 'this is it a this is it'
wanted = (
('D', 0, 11, 'this is it'),
('I', 0, 11, 'a'),
)
class MatchIsTooCheap(_Base, unittest.TestCase):
a = 'stdin.h'
b = 's'
wanted = (
('D', 0, 1, 'tdin.h'),
)
class MultiLine(_Base, unittest.TestCase):
a = 'hi first line\nsecond line first line\nsecond line world'
b = 'hi first line\nsecond line k world'
wanted = (
('D', 1, 12, 'first line'),
('D', 1, 12, '\n'),
('D', 1, 12, 'second line'),
('I', 1, 12, 'k'),
)
if __name__ == '__main__':
unittest.main()
# k = TestEditScript()
# unittest.TextTestRunner().run(k)

View File

@ -0,0 +1,82 @@
#!/usr/bin/env python
# encoding: utf-8
# pylint: skip-file
import unittest
from position import Position
class _MPBase(object):
def runTest(self):
obj = Position(*self.obj)
for pivot, delta, wanted in self.steps:
obj.move(Position(*pivot), Position(*delta))
self.assertEqual(Position(*wanted), obj)
class MovePosition_DelSameLine(_MPBase, unittest.TestCase):
# hello wor*ld -> h*ld -> hl*ld
obj = (0, 9)
steps = (
((0, 1), (0, -8), (0, 1)),
((0, 1), (0, 1), (0, 2)),
)
class MovePosition_DelSameLine1(_MPBase, unittest.TestCase):
# hel*lo world -> hel*world -> hel*worl
obj = (0, 3)
steps = (
((0, 4), (0, -3), (0, 3)),
((0, 8), (0, -1), (0, 3)),
)
class MovePosition_InsSameLine1(_MPBase, unittest.TestCase):
# hel*lo world -> hel*woresld
obj = (0, 3)
steps = (
((0, 4), (0, -3), (0, 3)),
((0, 6), (0, 2), (0, 3)),
((0, 8), (0, -1), (0, 3))
)
class MovePosition_InsSameLine2(_MPBase, unittest.TestCase):
# hello wor*ld -> helesdlo wor*ld
obj = (0, 9)
steps = (
((0, 3), (0, 3), (0, 12)),
)
class MovePosition_DelSecondLine(_MPBase, unittest.TestCase):
# hello world. sup hello world.*a, was
# *a, was ach nix
# ach nix
obj = (1, 0)
steps = (
((0, 12), (0, -4), (1, 0)),
((0, 12), (-1, 0), (0, 12)),
)
class MovePosition_DelSecondLine1(_MPBase, unittest.TestCase):
# hello world. sup
# a, *was
# ach nix
# hello world.a*was
# ach nix
obj = (1, 3)
steps = (
((0, 12), (0, -4), (1, 3)),
((0, 12), (-1, 0), (0, 15)),
((0, 12), (0, -3), (0, 12)),
((0, 12), (0, 1), (0, 13)),
)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,84 @@
#!/usr/bin/env python
# encoding: utf-8
"""Utilities to deal with text."""
def unescape(text):
"""Removes '\\' escaping from 'text'."""
rv = ''
i = 0
while i < len(text):
if i + 1 < len(text) and text[i] == '\\':
rv += text[i + 1]
i += 1
else:
rv += text[i]
i += 1
return rv
def escape(text, chars):
"""Escapes all characters in 'chars' in text using backspaces."""
rv = ''
for char in text:
if char in chars:
rv += '\\'
rv += char
return rv
def fill_in_whitespace(text):
"""Returns 'text' with escaped whitespace replaced through whitespaces."""
text = text.replace(r"\n", '\n')
text = text.replace(r"\t", '\t')
text = text.replace(r"\r", '\r')
text = text.replace(r"\a", '\a')
text = text.replace(r"\b", '\b')
return text
def head_tail(line):
"""Returns the first word in 'line' and the rest of 'line' or None if the
line is too short."""
generator = (t.strip() for t in line.split(None, 1))
head = next(generator).strip()
tail = ''
try:
tail = next(generator).strip()
except StopIteration:
pass
return head, tail
class LineIterator(object):
"""Convenience class that keeps track of line numbers in files."""
def __init__(self, text):
self._line_index = -1
self._lines = list(text.splitlines(True))
def __iter__(self):
return self
def __next__(self):
"""Returns the next line."""
if self._line_index + 1 < len(self._lines):
self._line_index += 1
return self._lines[self._line_index]
raise StopIteration()
next = __next__ # for python2
@property
def line_index(self):
"""The 1 based line index in the current file."""
return self._line_index + 1
def peek(self):
"""Returns the next line (if there is any, otherwise None) without
advancing the iterator."""
try:
return self._lines[self._line_index + 1]
except IndexError:
return None

View File

@ -0,0 +1,14 @@
#!/usr/bin/env python
# encoding: utf-8
"""Public facing classes for TextObjects."""
from UltiSnips.text_objects._escaped_char import EscapedChar
from UltiSnips.text_objects._mirror import Mirror
from UltiSnips.text_objects._python_code import PythonCode
from UltiSnips.text_objects._shell_code import ShellCode
from UltiSnips.text_objects._snippet_instance import SnippetInstance
from UltiSnips.text_objects._tabstop import TabStop
from UltiSnips.text_objects._transformation import Transformation
from UltiSnips.text_objects._viml_code import VimLCode
from UltiSnips.text_objects._visual import Visual

View File

@ -0,0 +1,386 @@
#!/usr/bin/env python
# encoding: utf-8
"""Base classes for all text objects."""
from UltiSnips import _vim
from UltiSnips.position import Position
def _calc_end(text, start):
"""Calculate the end position of the 'text' starting at 'start."""
if len(text) == 1:
new_end = start + Position(0, len(text[0]))
else:
new_end = Position(start.line + len(text) - 1, len(text[-1]))
return new_end
def _text_to_vim(start, end, text):
"""Copy the given text to the current buffer, overwriting the span 'start'
to 'end'."""
lines = text.split('\n')
new_end = _calc_end(lines, start)
before = _vim.buf[start.line][:start.col]
after = _vim.buf[end.line][end.col:]
new_lines = []
if len(lines):
new_lines.append(before + lines[0])
new_lines.extend(lines[1:])
new_lines[-1] += after
_vim.buf[start.line:end.line + 1] = new_lines
# Open any folds this might have created
_vim.buf.cursor = start
_vim.command('normal! zv')
return new_end
# These classes use their subclasses a lot and we really do not want to expose
# their functions more globally.
# pylint: disable=protected-access
class TextObject(object):
"""Represents any object in the text that has a span in any ways."""
def __init__(self, parent, token_or_start, end=None,
initial_text='', tiebreaker=None):
self._parent = parent
if end is not None: # Took 4 arguments
self._start = token_or_start
self._end = end
self._initial_text = initial_text
else: # Initialize from token
self._start = token_or_start.start
self._end = token_or_start.end
self._initial_text = token_or_start.initial_text
self._tiebreaker = tiebreaker or Position(
self._start.line, self._end.line)
if parent is not None:
parent._add_child(self)
def _move(self, pivot, diff):
"""Move this object by 'diff' while 'pivot' is the point of change."""
self._start.move(pivot, diff)
self._end.move(pivot, diff)
def __lt__(self, other):
me_tuple = (self.start.line, self.start.col,
self._tiebreaker.line, self._tiebreaker.col)
other_tuple = (other._start.line, other._start.col,
other._tiebreaker.line, other._tiebreaker.col)
return me_tuple < other_tuple
def __le__(self, other):
me_tuple = (self._start.line, self._start.col,
self._tiebreaker.line, self._tiebreaker.col)
other_tuple = (other._start.line, other._start.col,
other._tiebreaker.line, other._tiebreaker.col)
return me_tuple <= other_tuple
def __repr__(self):
ct = ''
try:
ct = self.current_text
except IndexError:
ct = '<err>'
return '%s(%r->%r,%r)' % (self.__class__.__name__,
self._start, self._end, ct)
@property
def current_text(self):
"""The current text of this object."""
if self._start.line == self._end.line:
return _vim.buf[self._start.line][self._start.col:self._end.col]
else:
lines = [_vim.buf[self._start.line][self._start.col:]]
lines.extend(_vim.buf[self._start.line + 1:self._end.line])
lines.append(_vim.buf[self._end.line][:self._end.col])
return '\n'.join(lines)
@property
def start(self):
"""The start position."""
return self._start
@property
def end(self):
"""The end position."""
return self._end
def overwrite(self, gtext=None):
"""Overwrite the text of this object in the Vim Buffer and update its
length information.
If 'gtext' is None use the initial text of this object.
"""
# We explicitly do not want to move our children around here as we
# either have non or we are replacing text initially which means we do
# not want to mess with their positions
if self.current_text == gtext:
return
old_end = self._end
self._end = _text_to_vim(
self._start, self._end, gtext or self._initial_text)
if self._parent:
self._parent._child_has_moved(
self._parent._children.index(self), min(old_end, self._end),
self._end.delta(old_end)
)
def _update(self, done):
"""Update this object inside the Vim Buffer.
Return False if you need to be called again for this edit cycle.
Otherwise return True.
"""
raise NotImplementedError('Must be implemented by subclasses.')
class EditableTextObject(TextObject):
"""This base class represents any object in the text that can be changed by
the user."""
def __init__(self, *args, **kwargs):
TextObject.__init__(self, *args, **kwargs)
self._children = []
self._tabstops = {}
##############
# Properties #
##############
@property
def children(self):
"""List of all children."""
return self._children
@property
def _editable_children(self):
"""List of all children that are EditableTextObjects."""
return [child for child in self._children if
isinstance(child, EditableTextObject)]
####################
# Public Functions #
####################
def find_parent_for_new_to(self, pos):
"""Figure out the parent object for something at 'pos'."""
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
###############################
# Private/Protected functions #
###############################
def _do_edit(self, cmd, ctab=None):
"""Apply the edit 'cmd' to this object."""
ctype, line, col, text = cmd
assert ('\n' not in text) or (text == '\n')
pos = Position(line, col)
to_kill = set()
new_cmds = []
for child in self._children:
if ctype == 'I': # Insertion
if (child._start < pos <
Position(child._end.line, child._end.col) and
isinstance(child, NoneditableTextObject)):
to_kill.add(child)
new_cmds.append(cmd)
break
elif ((child._start <= pos <= child._end) and
isinstance(child, EditableTextObject)):
if pos == child.end and not child.children:
try:
if ctab.number != child.number:
continue
except AttributeError:
pass
child._do_edit(cmd, ctab)
return
else: # Deletion
delend = pos + Position(0, len(text)) if text != '\n' \
else Position(line + 1, 0)
if ((child._start <= pos < child._end) and
(child._start < delend <= child._end)):
# this edit command is completely for the child
if isinstance(child, NoneditableTextObject):
to_kill.add(child)
new_cmds.append(cmd)
break
else:
child._do_edit(cmd, ctab)
return
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)
new_cmds.append(cmd)
break
elif (pos < child._start and
(child._start < delend <= child._end)):
# Case: partially for us, partially for the child
my_text = text[:(child._start - pos).col]
c_text = text[(child._start - pos).col:]
new_cmds.append((ctype, line, col, my_text))
new_cmds.append((ctype, line, col, c_text))
break
elif (delend >= child._end and (
child._start <= pos < child._end)):
# Case: partially for us, partially for the child
c_text = text[(child._end - pos).col:]
my_text = text[:(child._end - pos).col]
new_cmds.append((ctype, line, col, c_text))
new_cmds.append((ctype, line, col, my_text))
break
for child in to_kill:
self._del_child(child)
if len(new_cmds):
for child in new_cmds:
self._do_edit(child)
return
# We have to handle this ourselves
delta = Position(1, 0) if text == '\n' else Position(0, len(text))
if ctype == 'D':
# Makes no sense to delete in empty textobject
if self._start == self._end:
return
delta.line *= -1
delta.col *= -1
pivot = Position(line, col)
idx = -1
for cidx, child in enumerate(self._children):
if child._start < pivot <= child._end:
idx = cidx
self._child_has_moved(idx, pivot, delta)
def _move(self, pivot, diff):
TextObject._move(self, pivot, diff)
for child in self._children:
child._move(pivot, diff)
def _child_has_moved(self, idx, pivot, diff):
"""Called when a the child with 'idx' has moved behind 'pivot' by
'diff'."""
self._end.move(pivot, diff)
for child in self._children[idx + 1:]:
child._move(pivot, diff)
if self._parent:
self._parent._child_has_moved(
self._parent._children.index(self), pivot, diff
)
def _get_next_tab(self, number):
"""Returns the next tabstop after 'number'."""
if not len(self._tabstops.keys()):
return
tno_max = max(self._tabstops.keys())
possible_sol = []
i = number + 1
while i <= tno_max:
if i in self._tabstops:
possible_sol.append((i, self._tabstops[i]))
break
i += 1
child = [c._get_next_tab(number) for c in self._editable_children]
child = [c for c in child if c]
possible_sol += child
if not len(possible_sol):
return None
return min(possible_sol)
def _get_prev_tab(self, number):
"""Returns the previous tabstop before 'number'."""
if not len(self._tabstops.keys()):
return
tno_min = min(self._tabstops.keys())
possible_sol = []
i = number - 1
while i >= tno_min and i > 0:
if i in self._tabstops:
possible_sol.append((i, self._tabstops[i]))
break
i -= 1
child = [c._get_prev_tab(number) for c in self._editable_children]
child = [c for c in child if c]
possible_sol += child
if not len(possible_sol):
return None
return max(possible_sol)
def _get_tabstop(self, requester, number):
"""Returns the tabstop 'number'.
'requester' is the class that is interested in this.
"""
if number in self._tabstops:
return self._tabstops[number]
for child in self._editable_children:
if child is requester:
continue
rv = child._get_tabstop(self, number)
if rv is not None:
return rv
if self._parent and requester is not self._parent:
return self._parent._get_tabstop(self, number)
def _update(self, done):
if all((child in done) for child in self._children):
assert self not in done
done.add(self)
return True
def _add_child(self, child):
"""Add 'child' as a new child of this text object."""
self._children.append(child)
self._children.sort()
def _del_child(self, child):
"""Delete this 'child'."""
child._parent = None
self._children.remove(child)
# If this is a tabstop, delete it. Might have been deleted already if
# it was nested.
try:
del self._tabstops[child.number]
except (AttributeError, KeyError):
pass
class NoneditableTextObject(TextObject):
"""All passive text objects that the user can't edit by hand."""
def _update(self, done):
return True

View File

@ -0,0 +1,16 @@
#!/usr/bin/env python
# encoding: utf-8
"""See module comment."""
from UltiSnips.text_objects._base import NoneditableTextObject
class EscapedChar(NoneditableTextObject):
r"""
This class is a escape char like \$. It is handled in a text object to make
sure that siblings are correctly moved after replacing the text.
This is a base class without functionality just to mark it in the code.
"""

View File

@ -0,0 +1,35 @@
#!/usr/bin/env python
# encoding: utf-8
"""A Mirror object contains the same text as its related tabstop."""
from UltiSnips.text_objects._base import NoneditableTextObject
class Mirror(NoneditableTextObject):
"""See module docstring."""
def __init__(self, parent, tabstop, token):
NoneditableTextObject.__init__(self, parent, token)
self._ts = tabstop
def _update(self, done):
if self._ts.is_killed:
self.overwrite('')
self._parent._del_child(self) # pylint:disable=protected-access
return True
if self._ts not in done:
return False
self.overwrite(self._get_text())
return True
def _get_text(self):
"""Returns the text used for mirroring.
Overwritten by base classes.
"""
return self._ts.current_text

View File

@ -0,0 +1,321 @@
#!/usr/bin/env python
# encoding: utf-8
"""Implements `!p ` interpolation."""
import os
from collections import namedtuple
from UltiSnips import _vim
from UltiSnips.compatibility import as_unicode
from UltiSnips.indent_util import IndentUtil
from UltiSnips.text_objects._base import NoneditableTextObject
from UltiSnips.vim_state import _Placeholder
import UltiSnips.snippet_manager
class _Tabs(object):
"""Allows access to tabstop content via t[] inside of python code."""
def __init__(self, to):
self._to = to
def __getitem__(self, no):
ts = self._to._get_tabstop(
self._to,
int(no)) # pylint:disable=protected-access
if ts is None:
return ''
return ts.current_text
def __setitem__(self, no, value):
ts = self._to._get_tabstop(
self._to,
int(no)) # pylint:disable=protected-access
if ts is None:
return
ts.overwrite(value)
_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.
This is the 'snip' object in python code.
"""
def __init__(self, initial_indent, vmode, vtext, context, parent):
self._ind = IndentUtil()
self._visual = _VisualContent(vmode, vtext)
self._initial_indent = self._ind.indent_to_spaces(initial_indent)
self._reset('')
self._context = context
self._start = parent.start
self._end = parent.end
self._parent = parent
def _reset(self, cur):
"""Gets the snippet ready for another update.
:cur: the new value for c.
"""
self._ind.reset()
self._cur = cur
self._rv = ''
self._changed = False
self.reset_indent()
def shift(self, amount=1):
"""Shifts the indentation level. Note that this uses the shiftwidth
because thats what code formatters use.
:amount: the amount by which to shift.
"""
self.indent += ' ' * self._ind.shiftwidth * amount
def unshift(self, amount=1):
"""Unshift the indentation level. Note that this uses the shiftwidth
because thats what code formatters use.
:amount: the amount by which to unshift.
"""
by = -self._ind.shiftwidth * amount
try:
self.indent = self.indent[:by]
except IndexError:
self.indent = ''
def mkline(self, line='', indent=None):
"""Creates a properly set up line.
:line: the text to add
:indent: the indentation to have at the beginning
if None, it uses the default amount
"""
if indent is None:
indent = self.indent
# this deals with the fact that the first line is
# already properly indented
if '\n' not in self._rv:
try:
indent = indent[len(self._initial_indent):]
except IndexError:
indent = ''
indent = self._ind.spaces_to_indent(indent)
return indent + line
def reset_indent(self):
"""Clears the indentation."""
self.indent = self._initial_indent
# Utility methods
@property
def fn(self): # pylint:disable=no-self-use,invalid-name
"""The filename."""
return _vim.eval('expand("%:t")') or ''
@property
def basename(self): # pylint:disable=no-self-use
"""The filename without extension."""
return _vim.eval('expand("%:t:r")') or ''
@property
def ft(self): # pylint:disable=invalid-name
"""The filetype."""
return self.opt('&filetype', '')
@property
def rv(self): # pylint:disable=invalid-name
"""The return value.
The text to insert at the location of the placeholder.
"""
return self._rv
@rv.setter
def rv(self, value): # pylint:disable=invalid-name
"""See getter."""
self._changed = True
self._rv = value
@property
def _rv_changed(self):
"""True if rv has changed."""
return self._changed
@property
def c(self): # pylint:disable=invalid-name
"""The current text of the placeholder."""
return self._cur
@property
def v(self): # pylint:disable=invalid-name
"""Content of visual expansions."""
return self._visual
@property
def p(self):
if self._parent.current_placeholder:
return self._parent.current_placeholder
else:
return _Placeholder('', 0, 0)
@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':
try:
return _vim.eval(option)
except _vim.error:
pass
return default
def __add__(self, value):
"""Appends the given line to rv using mkline."""
self.rv += '\n' # pylint:disable=invalid-name
self.rv += self.mkline(value)
return self
def __lshift__(self, other):
"""Same as unshift."""
self.unshift(other)
def __rshift__(self, other):
"""Same as shift."""
self.shift(other)
@property
def snippet_start(self):
"""
Returns start of the snippet in format (line, column).
"""
return self._start
@property
def snippet_end(self):
"""
Returns end of the snippet in format (line, column).
"""
return self._end
@property
def buffer(self):
return _vim.buf
class PythonCode(NoneditableTextObject):
"""See module docstring."""
def __init__(self, parent, token):
# Find our containing snippet for snippet local data
snippet = parent
while snippet:
try:
self._locals = snippet.locals
text = snippet.visual_content.text
mode = snippet.visual_content.mode
context = snippet.context
break
except AttributeError as e:
snippet = snippet._parent # pylint:disable=protected-access
self._snip = SnippetUtil(token.indent, mode, text, context, snippet)
self._codes = ((
'import re, os, vim, string, random',
'\n'.join(snippet.globals.get('!p', [])).replace('\r\n', '\n'),
token.code.replace('\\`', '`')
))
NoneditableTextObject.__init__(self, parent, token)
def _update(self, done):
path = _vim.eval('expand("%")') or ''
ct = self.current_text
self._locals.update({
't': _Tabs(self._parent),
'fn': os.path.basename(path),
'path': path,
'cur': ct,
'res': ct,
'snip': self._snip,
})
self._snip._reset(ct) # pylint:disable=protected-access
for code in self._codes:
try:
exec(code, self._locals) # pylint:disable=exec-used
except Exception as e:
e.snippet_code = code
raise
rv = as_unicode(
self._snip.rv if self._snip._rv_changed # pylint:disable=protected-access
else as_unicode(self._locals['res'])
)
if ct != rv:
self.overwrite(rv)
return False
return True

View File

@ -0,0 +1,76 @@
#!/usr/bin/env python
# encoding: utf-8
"""Implements `echo hi` shell code interpolation."""
import os
import platform
from subprocess import Popen, PIPE
import stat
import tempfile
from UltiSnips.compatibility import as_unicode
from UltiSnips.text_objects._base import NoneditableTextObject
def _chomp(string):
"""Rather than rstrip(), remove only the last newline and preserve
purposeful whitespace."""
if len(string) and string[-1] == '\n':
string = string[:-1]
if len(string) and string[-1] == '\r':
string = string[:-1]
return string
def _run_shell_command(cmd, tmpdir):
"""Write the code to a temporary file."""
cmdsuf = ''
if platform.system() == 'Windows':
# suffix required to run command on windows
cmdsuf = '.bat'
# turn echo off
cmd = '@echo off\r\n' + cmd
handle, path = tempfile.mkstemp(text=True, dir=tmpdir, suffix=cmdsuf)
os.write(handle, cmd.encode('utf-8'))
os.close(handle)
os.chmod(path, stat.S_IRWXU)
# Execute the file and read stdout
proc = Popen(path, shell=True, stdout=PIPE, stderr=PIPE)
proc.wait()
stdout, _ = proc.communicate()
os.unlink(path)
return _chomp(as_unicode(stdout))
def _get_tmp():
"""Find an executable tmp directory."""
userdir = os.path.expanduser('~')
for testdir in [tempfile.gettempdir(), os.path.join(userdir, '.cache'),
os.path.join(userdir, '.tmp'), userdir]:
if (not os.path.exists(testdir) or
not _run_shell_command('echo success', testdir) == 'success'):
continue
return testdir
return ''
class ShellCode(NoneditableTextObject):
"""See module docstring."""
def __init__(self, parent, token):
NoneditableTextObject.__init__(self, parent, token)
self._code = token.code.replace('\\`', '`')
self._tmpdir = _get_tmp()
def _update(self, done):
if not self._tmpdir:
output = \
'Unable to find executable tmp directory, check noexec on /tmp'
else:
output = _run_shell_command(self._code, self._tmpdir)
self.overwrite(output)
self._parent._del_child(self) # pylint:disable=protected-access
return True

View File

@ -0,0 +1,152 @@
#!/usr/bin/env python
# encoding: utf-8
"""A Snippet instance is an instance of a Snippet Definition.
That is, when the user expands a snippet, a SnippetInstance is created
to keep track of the corresponding TextObjects. The Snippet itself is
also a TextObject.
"""
from UltiSnips import _vim
from UltiSnips.position import Position
from UltiSnips.text_objects._base import EditableTextObject, \
NoneditableTextObject
from UltiSnips.text_objects._tabstop import TabStop
class SnippetInstance(EditableTextObject):
"""See module docstring."""
# pylint:disable=protected-access
def __init__(self, snippet, parent, initial_text,
start, end, visual_content, last_re, globals, context):
if start is None:
start = Position(0, 0)
if end is None:
end = Position(0, 0)
self.snippet = snippet
self._cts = 0
self.context = context
self.locals = {'match': last_re, 'context': context}
self.globals = globals
self.visual_content = visual_content
self.current_placeholder = None
EditableTextObject.__init__(self, parent, start, end, initial_text)
def replace_initial_text(self):
"""Puts the initial text of all text elements into Vim."""
def _place_initial_text(obj):
"""recurses on the children to do the work."""
obj.overwrite()
if isinstance(obj, EditableTextObject):
for child in obj._children:
_place_initial_text(child)
_place_initial_text(self)
def replay_user_edits(self, cmds, ctab=None):
"""Replay the edits the user has done to keep endings of our Text
objects in sync with reality."""
for cmd in cmds:
self._do_edit(cmd, ctab)
def update_textobjects(self):
"""Update the text objects that should change automagically after the
users edits have been replayed.
This might also move the Cursor
"""
vc = _VimCursor(self)
done = set()
not_done = set()
def _find_recursive(obj):
"""Finds all text objects and puts them into 'not_done'."""
if isinstance(obj, EditableTextObject):
for child in obj._children:
_find_recursive(child)
not_done.add(obj)
_find_recursive(self)
counter = 10
while (done != not_done) and counter:
# Order matters for python locals!
for obj in sorted(not_done - done):
if obj._update(done):
done.add(obj)
counter -= 1
if not counter:
raise RuntimeError(
'The snippets content did not converge: Check for Cyclic '
'dependencies or random strings in your snippet. You can use '
"'if not snip.c' to make sure to only expand random output "
'once.')
vc.to_vim()
self._del_child(vc)
def select_next_tab(self, backwards=False):
"""Selects the next tabstop or the previous if 'backwards' is True."""
if self._cts is None:
return
if backwards:
cts_bf = self._cts
res = self._get_prev_tab(self._cts)
if res is None:
self._cts = cts_bf
return self._tabstops.get(self._cts, None)
self._cts, ts = res
return ts
else:
res = self._get_next_tab(self._cts)
if res is None:
self._cts = None
ts = self._get_tabstop(self, 0)
if ts:
return ts
# TabStop 0 was deleted. It was probably killed through some
# edit action. Recreate it at the end of us.
start = Position(self.end.line, self.end.col)
end = Position(self.end.line, self.end.col)
return TabStop(self, 0, start, end)
else:
self._cts, ts = res
return ts
return self._tabstops[self._cts]
def _get_tabstop(self, requester, no):
# SnippetInstances are completely self contained, therefore, we do not
# need to ask our parent for Tabstops
cached_parent = self._parent
self._parent = None
rv = EditableTextObject._get_tabstop(self, requester, no)
self._parent = cached_parent
return rv
def get_tabstops(self):
return self._tabstops
class _VimCursor(NoneditableTextObject):
"""Helper class to keep track of the Vim Cursor when text objects expand
and move."""
def __init__(self, parent):
NoneditableTextObject.__init__(
self, parent, _vim.buf.cursor, _vim.buf.cursor,
tiebreaker=Position(-1, -1))
def to_vim(self):
"""Moves the cursor in the Vim to our position."""
assert self._start == self._end
_vim.buf.cursor = self._start

View File

@ -0,0 +1,45 @@
#!/usr/bin/env python
# encoding: utf-8
"""This is the most important TextObject.
A TabStop is were the cursor comes to rest when the user taps through
the Snippet.
"""
from UltiSnips.text_objects._base import EditableTextObject
class TabStop(EditableTextObject):
"""See module docstring."""
def __init__(self, parent, token, start=None, end=None):
if start is not None:
self._number = token
EditableTextObject.__init__(self, parent, start, end)
else:
self._number = token.number
EditableTextObject.__init__(self, parent, token)
parent._tabstops[
self._number] = self # pylint:disable=protected-access
@property
def number(self):
"""The tabstop number."""
return self._number
@property
def is_killed(self):
"""True if this tabstop has been typed over and the user therefore can
no longer jump to it."""
return self._parent is None
def __repr__(self):
try:
text = self.current_text
except IndexError:
text = '<err>'
return 'TabStop(%s,%r->%r,%r)' % (self.number, self._start,
self._end, text)

View File

@ -0,0 +1,174 @@
#!/usr/bin/env python
# encoding: utf-8
"""Implements TabStop transformations."""
import re
import sys
from UltiSnips.text import unescape, fill_in_whitespace
from UltiSnips.text_objects._mirror import Mirror
def _find_closing_brace(string, start_pos):
"""Finds the corresponding closing brace after start_pos."""
bracks_open = 1
escaped = False
for idx, char in enumerate(string[start_pos:]):
if char == '(':
if not escaped:
bracks_open += 1
elif char == ')':
if not escaped:
bracks_open -= 1
if not bracks_open:
return start_pos + idx + 1
if char == '\\':
escaped = not escaped
else:
escaped = False
def _split_conditional(string):
"""Split the given conditional 'string' into its arguments."""
bracks_open = 0
args = []
carg = ''
escaped = False
for idx, char in enumerate(string):
if char == '(':
if not escaped:
bracks_open += 1
elif char == ')':
if not escaped:
bracks_open -= 1
elif char == ':' and not bracks_open and not escaped:
args.append(carg)
carg = ''
escaped = False
continue
carg += char
if char == '\\':
escaped = not escaped
else:
escaped = False
args.append(carg)
return args
def _replace_conditional(match, string):
"""Replaces a conditional match in a transformation."""
conditional_match = _CONDITIONAL.search(string)
while conditional_match:
start = conditional_match.start()
end = _find_closing_brace(string, start + 4)
args = _split_conditional(string[start + 4:end - 1])
rv = ''
if match.group(int(conditional_match.group(1))):
rv = unescape(_replace_conditional(match, args[0]))
elif len(args) > 1:
rv = unescape(_replace_conditional(match, args[1]))
string = string[:start] + rv + string[end:]
conditional_match = _CONDITIONAL.search(string)
return string
_ONE_CHAR_CASE_SWITCH = re.compile(r"\\([ul].)", re.DOTALL)
_LONG_CASEFOLDINGS = re.compile(r"\\([UL].*?)\\E", re.DOTALL)
_DOLLAR = re.compile(r"\$(\d+)", re.DOTALL)
_CONDITIONAL = re.compile(r"\(\?(\d+):", re.DOTALL)
class _CleverReplace(object):
"""Mimics TextMates replace syntax."""
def __init__(self, expression):
self._expression = expression
def replace(self, match):
"""Replaces 'match' through the correct replacement string."""
transformed = self._expression
# Replace all $? with capture groups
transformed = _DOLLAR.subn(
lambda m: match.group(int(m.group(1))), transformed)[0]
# Replace Case switches
def _one_char_case_change(match):
"""Replaces one character case changes."""
if match.group(1)[0] == 'u':
return match.group(1)[-1].upper()
else:
return match.group(1)[-1].lower()
transformed = _ONE_CHAR_CASE_SWITCH.subn(
_one_char_case_change, transformed)[0]
def _multi_char_case_change(match):
"""Replaces multi character case changes."""
if match.group(1)[0] == 'U':
return match.group(1)[1:].upper()
else:
return match.group(1)[1:].lower()
transformed = _LONG_CASEFOLDINGS.subn(
_multi_char_case_change, transformed)[0]
transformed = _replace_conditional(match, transformed)
return unescape(fill_in_whitespace(transformed))
# flag used to display only one time the lack of unidecode
UNIDECODE_ALERT_RAISED = False
class TextObjectTransformation(object):
"""Base class for Transformations and ${VISUAL}."""
def __init__(self, token):
self._convert_to_ascii = False
self._find = None
if token.search is None:
return
flags = 0
self._match_this_many = 1
if token.options:
if 'g' in token.options:
self._match_this_many = 0
if 'i' in token.options:
flags |= re.IGNORECASE
if 'm' in token.options:
flags |= re.MULTILINE
if 'a' in token.options:
self._convert_to_ascii = True
self._find = re.compile(token.search, flags | re.DOTALL)
self._replace = _CleverReplace(token.replace)
def _transform(self, text):
"""Do the actual transform on the given text."""
global UNIDECODE_ALERT_RAISED # pylint:disable=global-statement
if self._convert_to_ascii:
try:
import unidecode
text = unidecode.unidecode(text)
except Exception: # pylint:disable=broad-except
if UNIDECODE_ALERT_RAISED == False:
UNIDECODE_ALERT_RAISED = True
sys.stderr.write(
'Please install unidecode python package in order to '
'be able to make ascii conversions.\n')
if self._find is None:
return text
return self._find.subn(
self._replace.replace, text, self._match_this_many)[0]
class Transformation(Mirror, TextObjectTransformation):
"""See module docstring."""
def __init__(self, parent, ts, token):
Mirror.__init__(self, parent, ts, token)
TextObjectTransformation.__init__(self, token)
def _get_text(self):
return self._transform(self._ts.current_text)

View File

@ -0,0 +1,21 @@
#!/usr/bin/env python
# encoding: utf-8
"""Implements `!v ` VimL interpolation."""
from UltiSnips import _vim
from UltiSnips.text_objects._base import NoneditableTextObject
class VimLCode(NoneditableTextObject):
"""See module docstring."""
def __init__(self, parent, token):
self._code = token.code.replace('\\`', '`').strip()
NoneditableTextObject.__init__(self, parent, token)
def _update(self, done):
self.overwrite(_vim.eval(self._code))
return True

View File

@ -0,0 +1,64 @@
#!/usr/bin/env python
# encoding: utf-8
"""A ${VISUAL} placeholder that will use the text that was last visually
selected and insert it here.
If there was no text visually selected, this will be the empty string.
"""
import re
import textwrap
from UltiSnips import _vim
from UltiSnips.indent_util import IndentUtil
from UltiSnips.text_objects._transformation import TextObjectTransformation
from UltiSnips.text_objects._base import NoneditableTextObject
_REPLACE_NON_WS = re.compile(r"[^ \t]")
class Visual(NoneditableTextObject, TextObjectTransformation):
"""See module docstring."""
def __init__(self, parent, token):
# Find our containing snippet for visual_content
snippet = parent
while snippet:
try:
self._text = snippet.visual_content.text
self._mode = snippet.visual_content.mode
break
except AttributeError:
snippet = snippet._parent # pylint:disable=protected-access
if not self._text:
self._text = token.alternative_text
self._mode = 'v'
NoneditableTextObject.__init__(self, parent, token)
TextObjectTransformation.__init__(self, token)
def _update(self, done):
if self._mode == 'v': # Normal selection.
text = self._text
else: # Block selection or line selection.
text_before = _vim.buf[self.start.line][:self.start.col]
indent = _REPLACE_NON_WS.sub(' ', text_before)
iu = IndentUtil()
indent = iu.indent_to_spaces(indent)
indent = iu.spaces_to_indent(indent)
text = ''
for idx, line in enumerate(textwrap.dedent(
self._text).splitlines(True)):
if idx != 0:
text += indent
text += line
text = text[:-1] # Strip final '\n'
text = self._transform(text)
self.overwrite(text)
self._parent._del_child(self) # pylint:disable=protected-access
return True

View File

@ -0,0 +1,163 @@
#!/usr/bin/env python
# encoding: utf-8
"""Some classes to conserve Vim's state for comparing over time."""
from collections import deque, namedtuple
from UltiSnips import _vim
from UltiSnips.compatibility import as_unicode, byte2col
from UltiSnips.position import Position
_Placeholder = namedtuple('_FrozenPlaceholder', ['current_text', 'start', 'end'])
class VimPosition(Position):
"""Represents the current position in the buffer, together with some status
variables that might change our decisions down the line."""
def __init__(self):
pos = _vim.buf.cursor
self._mode = _vim.eval('mode()')
Position.__init__(self, pos.line, pos.col)
@property
def mode(self):
"""Returns the mode() this position was created."""
return self._mode
class VimState(object):
"""Caches some state information from Vim to better guess what editing
tasks the user might have done in the last step."""
def __init__(self):
self._poss = deque(maxlen=5)
self._lvb = None
self._text_to_expect = ''
self._unnamed_reg_cached = False
# We store the cached value of the unnamed register in Vim directly to
# avoid any Unicode issues with saving and restoring the unnamed
# register across the Python bindings. The unnamed register can contain
# data that cannot be coerced to Unicode, and so a simple vim.eval('@"')
# fails badly. Keeping the cached value in Vim directly, sidesteps the
# problem.
_vim.command('let g:_ultisnips_unnamed_reg_cache = ""')
def remember_unnamed_register(self, text_to_expect):
"""Save the unnamed register.
'text_to_expect' is text that we expect
to be contained in the register the next time this method is called -
this could be text from the tabstop that was selected and might have
been overwritten. We will not cache that then.
"""
self._unnamed_reg_cached = True
escaped_text = self._text_to_expect.replace("'", "''")
res = int(_vim.eval('@" != ' + "'" + escaped_text + "'"))
if res:
_vim.command('let g:_ultisnips_unnamed_reg_cache = @"')
self._text_to_expect = text_to_expect
def restore_unnamed_register(self):
"""Restores the unnamed register and forgets what we cached."""
if not self._unnamed_reg_cached:
return
_vim.command('let @" = g:_ultisnips_unnamed_reg_cache')
self._unnamed_reg_cached = False
def remember_position(self):
"""Remember the current position as a previous pose."""
self._poss.append(VimPosition())
def remember_buffer(self, to):
"""Remember the content of the buffer and the position."""
self._lvb = _vim.buf[to.start.line:to.end.line + 1]
self._lvb_len = len(_vim.buf)
self.remember_position()
@property
def diff_in_buffer_length(self):
"""Returns the difference in the length of the current buffer compared
to the remembered."""
return len(_vim.buf) - self._lvb_len
@property
def pos(self):
"""The last remembered position."""
return self._poss[-1]
@property
def ppos(self):
"""The second to last remembered position."""
return self._poss[-2]
@property
def remembered_buffer(self):
"""The content of the remembered buffer."""
return self._lvb[:]
class VisualContentPreserver(object):
"""Saves the current visual selection and the selection mode it was done in
(e.g. line selection, block selection or regular selection.)"""
def __init__(self):
self.reset()
def reset(self):
"""Forget the preserved state."""
self._mode = ''
self._text = as_unicode('')
self._placeholder = None
def conserve(self):
"""Save the last visual selection ond the mode it was made in."""
sl, sbyte = map(int,
(_vim.eval("""line("'<")"""), _vim.eval("""col("'<")""")))
el, ebyte = map(int,
(_vim.eval("""line("'>")"""), _vim.eval("""col("'>")""")))
sc = byte2col(sl, sbyte - 1)
ec = byte2col(el, ebyte - 1)
self._mode = _vim.eval('visualmode()')
_vim_line_with_eol = lambda ln: _vim.buf[ln] + '\n'
if sl == el:
text = _vim_line_with_eol(sl - 1)[sc:ec + 1]
else:
text = _vim_line_with_eol(sl - 1)[sc:]
for cl in range(sl, el - 1):
text += _vim_line_with_eol(cl)
text += _vim_line_with_eol(el - 1)[:ec + 1]
self._text = text
def conserve_placeholder(self, placeholder):
if placeholder:
self._placeholder = _Placeholder(
placeholder.current_text,
placeholder.start,
placeholder.end
)
else:
self._placeholder = None
@property
def text(self):
"""The conserved text."""
return self._text
@property
def mode(self):
"""The conserved visualmode()."""
return self._mode
@property
def placeholder(self):
"""Returns latest selected placeholder."""
return self._placeholder