# vim:fileencoding=utf-8:noet
from __future__ import unicode_literals, absolute_import, division
import os
try:
import vim
except ImportError:
vim = {} # NOQA
from powerline.bindings.vim import (vim_get_func, getbufvar, vim_getbufoption,
buffer_name, vim_getwinvar)
from powerline.theme import requires_segment_info
from powerline.lib import add_divider_highlight_group
from powerline.lib.vcs import guess, tree_status
from powerline.lib.humanize_bytes import humanize_bytes
from powerline.lib import wraps_saveargs as wraps
from collections import defaultdict
vim_funcs = {
'virtcol': vim_get_func('virtcol', rettype=int),
'getpos': vim_get_func('getpos'),
'fnamemodify': vim_get_func('fnamemodify'),
'expand': vim_get_func('expand'),
'bufnr': vim_get_func('bufnr', rettype=int),
'line2byte': vim_get_func('line2byte', rettype=int),
'line': vim_get_func('line', rettype=int),
}
vim_modes = {
'n': 'NORMAL',
'no': 'N·OPER',
'v': 'VISUAL',
'V': 'V·LINE',
'^V': 'V·BLCK',
's': 'SELECT',
'S': 'S·LINE',
'^S': 'S·BLCK',
'i': 'INSERT',
'R': 'REPLACE',
'Rv': 'V·RPLCE',
'c': 'COMMND',
'cv': 'VIM EX',
'ce': 'EX',
'r': 'PROMPT',
'rm': 'MORE',
'r?': 'CONFIRM',
'!': 'SHELL',
}
eventfuncs = defaultdict(lambda: [])
bufeventfuncs = defaultdict(lambda: [])
defined_events = set()
# TODO Remove cache when needed
def window_cached(func):
cache = {}
@requires_segment_info
@wraps(func)
def ret(segment_info, **kwargs):
window_id = segment_info['window_id']
if segment_info['mode'] == 'nc':
return cache.get(window_id)
else:
r = func(**kwargs)
cache[window_id] = r
return r
return ret
@requires_segment_info
[docs]def mode(pl, segment_info, override=None):
'''Return the current vim mode.
:param dict override:
dict for overriding default mode strings, e.g. ``{ 'n': 'NORM' }``
'''
mode = segment_info['mode']
if mode == 'nc':
return None
if not override:
return vim_modes[mode]
try:
return override[mode]
except KeyError:
return vim_modes[mode]
@requires_segment_info
[docs]def visual_range(pl, segment_info):
'''Return the current visual selection range.
Returns a value similar to `showcmd`.
'''
if segment_info['mode'] not in ('v', 'V', '^V'):
return None
pos_start = vim_funcs['getpos']('v')
pos_end = vim_funcs['getpos']('.')
# Workaround for vim's "excellent" handling of multibyte characters and display widths
pos_start[2] = vim_funcs['virtcol']([pos_start[1], pos_start[2], pos_start[3]])
pos_end[2] = vim_funcs['virtcol']([pos_end[1], pos_end[2], pos_end[3]])
visual_start = (int(pos_start[1]), int(pos_start[2]))
visual_end = (int(pos_end[1]), int(pos_end[2]))
diff_rows = abs(visual_end[0] - visual_start[0]) + 1
diff_cols = abs(visual_end[1] - visual_start[1]) + 1
if segment_info['mode'] == '^V':
return '{0} × {1}'.format(diff_rows, diff_cols)
elif segment_info['mode'] == 'V' or diff_rows > 1:
return '{0} rows'.format(diff_rows)
else:
return '{0} cols'.format(diff_cols)
@requires_segment_info
[docs]def modified_indicator(pl, segment_info, text='+'):
'''Return a file modified indicator.
:param string text:
text to display if the current buffer is modified
'''
return text if int(vim_getbufoption(segment_info, 'modified')) else None
@requires_segment_info
[docs]def paste_indicator(pl, segment_info, text='PASTE'):
'''Return a paste mode indicator.
:param string text:
text to display if paste mode is enabled
'''
return text if int(vim.eval('&paste')) else None
@requires_segment_info
[docs]def readonly_indicator(pl, segment_info, text=''):
'''Return a read-only indicator.
:param string text:
text to display if the current buffer is read-only
'''
return text if int(vim_getbufoption(segment_info, 'readonly')) else None
@requires_segment_info
[docs]def file_directory(pl, segment_info, shorten_user=True, shorten_cwd=True, shorten_home=False):
'''Return file directory (head component of the file path).
:param bool shorten_user:
shorten ``$HOME`` directory to :file:`~/`
:param bool shorten_cwd:
shorten current directory to :file:`./`
:param bool shorten_home:
shorten all directories in :file:`/home/` to :file:`~user/` instead of :file:`/home/user/`.
'''
name = buffer_name(segment_info['buffer'])
if not name:
return None
import sys
file_directory = vim_funcs['fnamemodify'](name, (':~' if shorten_user else '')
+ (':.' if shorten_cwd else '') + ':h')
if not file_directory:
return None
if shorten_home and file_directory.startswith('/home/'):
file_directory = b'~' + file_directory[6:]
file_directory = file_directory.decode('utf-8', 'powerline_vim_strtrans_error')
return file_directory + os.sep
@requires_segment_info
[docs]def file_name(pl, segment_info, display_no_file=False, no_file_text='[No file]'):
'''Return file name (tail component of the file path).
:param bool display_no_file:
display a string if the buffer is missing a file name
:param str no_file_text:
the string to display if the buffer is missing a file name
Highlight groups used: ``file_name_no_file`` or ``file_name``, ``file_name``.
'''
name = buffer_name(segment_info['buffer'])
if not name:
if display_no_file:
return [{
'contents': no_file_text,
'highlight_group': ['file_name_no_file', 'file_name'],
}]
else:
return None
return os.path.basename(name).decode('utf-8', 'powerline_vim_strtrans_error')
@window_cached
[docs]def file_size(pl, suffix='B', si_prefix=False):
'''Return file size in &encoding.
:param str suffix:
string appended to the file size
:param bool si_prefix:
use SI prefix, e.g. MB instead of MiB
:return: file size or None if the file isn't saved or if the size is too big to fit in a number
'''
# Note: returns file size in &encoding, not in &fileencoding. But returned
# size is updated immediately; and it is valid for any buffer
file_size = vim_funcs['line2byte'](len(vim.current.buffer) + 1) - 1
if file_size < 0:
file_size = 0
return humanize_bytes(file_size, suffix, si_prefix)
@requires_segment_info
@add_divider_highlight_group('background:divider')
@requires_segment_info
@add_divider_highlight_group('background:divider')
[docs]def file_encoding(pl, segment_info):
'''Return file encoding/character set.
:return: file encoding/character set or None if unknown or missing file encoding
Divider highlight group used: ``background:divider``.
'''
return vim_getbufoption(segment_info, 'fileencoding') or None
@requires_segment_info
@add_divider_highlight_group('background:divider')
[docs]def file_type(pl, segment_info):
'''Return file type.
:return: file type or None if unknown file type
Divider highlight group used: ``background:divider``.
'''
return vim_getbufoption(segment_info, 'filetype') or None
@requires_segment_info
[docs]def window_title(pl, segment_info):
'''Return the window title.
This currently looks at the ``quickfix_title`` window variable,
which is used by Syntastic and Vim itself.
It is used in the quickfix theme.'''
try:
return vim_getwinvar(segment_info, 'quickfix_title')
except KeyError:
return None
@requires_segment_info
[docs]def line_percent(pl, segment_info, gradient=False):
'''Return the cursor position in the file as a percentage.
:param bool gradient:
highlight the percentage with a color gradient (by default a green to red gradient)
Highlight groups used: ``line_percent_gradient`` (gradient), ``line_percent``.
'''
line_current = segment_info['window'].cursor[0]
line_last = len(segment_info['buffer'])
percentage = line_current * 100.0 / line_last
if not gradient:
return str(int(round(percentage)))
return [{
'contents': str(int(round(percentage))),
'highlight_group': ['line_percent_gradient', 'line_percent'],
'gradient_level': percentage,
}]
@window_cached
[docs]def position(pl, position_strings={'top':'Top', 'bottom':'Bot', 'all':'All'}, gradient=False):
'''Return the position of the current view in the file as a percentage.
:param dict position_strings:
dict for translation of the position strings, e.g. ``{"top":"Oben", "bottom":"Unten", "all":"Alles"}``
:param bool gradient:
highlight the percentage with a color gradient (by default a green to red gradient)
Highlight groups used: ``position_gradient`` (gradient), ``position``.
'''
line_last = len(vim.current.buffer)
winline_first = vim_funcs['line']('w0')
winline_last = vim_funcs['line']('w$')
if winline_first == 1 and winline_last == line_last:
percentage = 0.0
content = position_strings['all']
elif winline_first == 1:
percentage = 0.0
content = position_strings['top']
elif winline_last == line_last:
percentage = 100.0
content = position_strings['bottom']
else:
percentage = winline_first * 100.0 / (line_last - winline_last + winline_first)
content = str(int(round(percentage))) + '%'
if not gradient:
return content
return [{
'contents': content,
'highlight_group': ['position_gradient', 'position'],
'gradient_level': percentage,
}]
@requires_segment_info
[docs]def line_current(pl, segment_info):
'''Return the current cursor line.'''
return str(segment_info['window'].cursor[0])
@requires_segment_info
[docs]def col_current(pl, segment_info):
'''Return the current cursor column.
'''
return str(segment_info['window'].cursor[1] + 1)
# TODO Add &textwidth-based gradient
@window_cached
[docs]def virtcol_current(pl, gradient=True):
'''Return current visual column with concealed characters ingored
:param bool gradient:
Determines whether it should show textwidth-based gradient (gradient level is ``virtcol * 100 / textwidth``).
Highlight groups used: ``virtcol_current_gradient`` (gradient), ``virtcol_current`` or ``col_current``.
'''
col = vim_funcs['virtcol']('.')
r = [{'contents': str(col), 'highlight_group': ['virtcol_current', 'col_current']}]
if gradient:
textwidth = int(getbufvar('%', '&textwidth'))
r[-1]['gradient_level'] = min(col * 100 / textwidth, 100) if textwidth else 0
r[-1]['highlight_group'].insert(0, 'virtcol_current_gradient')
return r
[docs]def modified_buffers(pl, text='+ ', join_str=','):
'''Return a comma-separated list of modified buffers.
:param str text:
text to display before the modified buffer list
:param str join_str:
string to use for joining the modified buffer list
'''
buffer_len = vim_funcs['bufnr']('$')
buffer_mod = [str(bufnr) for bufnr in range(1, buffer_len + 1) if int(getbufvar(bufnr, '&modified') or 0)]
if buffer_mod:
return text + join_str.join(buffer_mod)
return None
@requires_segment_info
[docs]def branch(pl, segment_info, status_colors=False):
'''Return the current working branch.
:param bool status_colors:
determines whether repository status will be used to determine highlighting. Default: False.
Highlight groups used: ``branch_clean``, ``branch_dirty``, ``branch``.
Divider highlight group used: ``branch:divider``.
'''
name = segment_info['buffer'].name
skip = not (name and (not vim_getbufoption(segment_info, 'buftype')))
if not skip:
repo = guess(path=name)
if repo is not None:
branch = repo.branch()
scol = ['branch']
if status_colors:
status = tree_status(repo, pl)
scol.insert(0, 'branch_dirty' if status and status.strip() else 'branch_clean')
return [{
'contents': branch,
'highlight_group': scol,
'divider_highlight_group': 'branch:divider',
}]
@requires_segment_info
[docs]def file_vcs_status(pl, segment_info):
'''Return the VCS status for this buffer.
Highlight groups used: ``file_vcs_status``.
'''
name = segment_info['buffer'].name
skip = not (name and (not vim_getbufoption(segment_info, 'buftype')))
if not skip:
repo = guess(path=name)
if repo is not None:
status = repo.status(os.path.relpath(name, repo.directory))
if not status:
return None
status = status.strip()
ret = []
for status in status:
ret.append({
'contents': status,
'highlight_group': ['file_vcs_status_' + status, 'file_vcs_status'],
})
return ret