dotfiles/vim/plugins/ultisnips/pythonx/UltiSnips/buffer_proxy.py
2018-04-05 13:06:54 +02:00

230 lines
7.3 KiB
Python

# 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