You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
295 lines
11 KiB
Python
295 lines
11 KiB
Python
#!/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
|
|
|
|
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."""
|
|
# Note: we want byte position here
|
|
_, col = vim.current.window.cursor
|
|
line = vim.current.line
|
|
before = as_unicode(line[:col])
|
|
return before
|
|
|
|
@property
|
|
def number(self): # pylint:disable=no-self-use
|
|
"""The bufnr() of this buffer."""
|
|
return int(eval("bufnr('%')"))
|
|
|
|
@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
|
|
|
|
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 feedkeys(keys, mode='n'):
|
|
"""Wrapper around vim's feedkeys function. Mainly for convenience."""
|
|
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 select(start, end):
|
|
"""Select the span in Select mode"""
|
|
|
|
_unmap_select_mode_mapping()
|
|
|
|
delta = end - start
|
|
lineno, col = start.line, start.col
|
|
|
|
col = col2byte(lineno + 1, col)
|
|
vim.current.window.cursor = lineno + 1, col
|
|
|
|
move_cmd = ""
|
|
if eval("mode()") != 'n':
|
|
move_cmd += r"\<Esc>"
|
|
|
|
# Case 1: Zero Length Tabstops
|
|
if delta.line == delta.col == 0:
|
|
if col == 0 or eval("mode()") not in 'i' and \
|
|
col < len(buf[lineno]):
|
|
move_cmd += "i"
|
|
else:
|
|
move_cmd += "a"
|
|
else:
|
|
# Case 2a: Non zero length
|
|
# If a tabstop immediately starts with a newline, the selection must
|
|
# start after the last character in the current line. But if we are in
|
|
# insert mode and <Esc> out of it, we cannot go past the last character
|
|
# with move_one_right and therefore cannot visual-select this newline.
|
|
# We have to hack around this by adding an extra space which we can
|
|
# select. Note that this problem could be circumvent by selecting the
|
|
# tab backwards (that is starting at the end); one would not need to
|
|
# modify the line for this. This creates other trouble though
|
|
if col >= len(buf[lineno]):
|
|
buf[lineno] += " "
|
|
|
|
if delta.line:
|
|
move_lines = "%ij" % delta.line
|
|
else:
|
|
move_lines = ""
|
|
# Depending on the current mode and position, we
|
|
# might need to move escape out of the mode and this
|
|
# will move our cursor one left
|
|
if col != 0 and eval("mode()") == 'i':
|
|
move_one_right = "l"
|
|
else:
|
|
move_one_right = ""
|
|
|
|
# After moving to the correct line, we go back to column 0
|
|
# and select right from there. Note that the we have to select
|
|
# one column less since Vim's visual selection is including the
|
|
# ending while Python slicing is excluding the ending.
|
|
inclusive = "inclusive" in eval("&selection")
|
|
if end.col == 0:
|
|
# Selecting should end at beginning of line -> Select the
|
|
# previous line till its end
|
|
do_select = "k$"
|
|
if not inclusive:
|
|
do_select += "j0"
|
|
elif end.col > 1:
|
|
do_select = "0%il" % (end.col-1 if inclusive else end.col)
|
|
else:
|
|
do_select = "0" if inclusive else "0l"
|
|
|
|
move_cmd += _LangMapTranslator().translate(
|
|
r"%sv%s%s\<c-g>" % (move_one_right, move_lines, do_select)
|
|
)
|
|
|
|
feedkeys(move_cmd)
|
|
|
|
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
|
|
all_maps = list(filter(len, eval(r"_tmp_smaps").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
|
|
|
|
class _RealLangMapTranslator(object):
|
|
"""This cares for the Vim langmap option and basically reverses the
|
|
mappings. This was the only solution to get UltiSnips to work nicely with
|
|
langmap; other stuff I tried was using inoremap movement commands and
|
|
caching and restoring the langmap option.
|
|
|
|
Note that this will not work if the langmap overwrites a character
|
|
completely, for example if 'j' is remapped, but nothing is mapped back to
|
|
'j', then moving one line down is no longer possible and UltiSnips will
|
|
fail.
|
|
"""
|
|
_maps = {}
|
|
_SEMICOLONS = re.compile(r"(?<!\\);")
|
|
_COMMA = re.compile(r"(?<!\\),")
|
|
|
|
def _create_translation(self, langmap):
|
|
"""Create the reverse mapping from 'langmap'."""
|
|
from_chars, to_chars = "", ""
|
|
for char in self._COMMA.split(langmap):
|
|
char = char.replace("\\,", ",")
|
|
res = self._SEMICOLONS.split(char)
|
|
if len(res) > 1:
|
|
from_char, to_char = [a.replace("\\;", ";") for a in res]
|
|
from_chars += from_char
|
|
to_chars += to_char
|
|
else:
|
|
from_chars += char[::2]
|
|
to_chars += char[1::2]
|
|
self._maps[langmap] = (from_chars, to_chars)
|
|
|
|
def translate(self, text):
|
|
"""Inverse map 'text' through langmap."""
|
|
langmap = eval("&langmap").strip()
|
|
if langmap == "":
|
|
return text
|
|
text = as_unicode(text)
|
|
if langmap not in self._maps:
|
|
self._create_translation(langmap)
|
|
for before, after in zip(*self._maps[langmap]):
|
|
text = text.replace(before, after)
|
|
return text
|
|
|
|
class _DummyLangMapTranslator(object):
|
|
"""If vim hasn't got the langmap compiled in, we never have to do anything.
|
|
Then this class is used. """
|
|
translate = lambda self, s: s
|
|
|
|
_LangMapTranslator = _RealLangMapTranslator
|
|
if not int(eval('has("langmap")')):
|
|
_LangMapTranslator = _DummyLangMapTranslator
|