diff options
Diffstat (limited to 'powerline/vim.py')
-rw-r--r-- | powerline/vim.py | 359 |
1 files changed, 359 insertions, 0 deletions
diff --git a/powerline/vim.py b/powerline/vim.py new file mode 100644 index 0000000..2638c91 --- /dev/null +++ b/powerline/vim.py @@ -0,0 +1,359 @@ +# vim:fileencoding=utf-8:noet +from __future__ import (unicode_literals, division, absolute_import, print_function) + +import sys +import json +import logging + +from itertools import count + +try: + import vim +except ImportError: + vim = object() + +from powerline.bindings.vim import vim_get_func, vim_getvar, get_vim_encoding, python_to_vim +from powerline import Powerline, FailedUnicode, finish_common_config +from powerline.lib.dict import mergedicts +from powerline.lib.unicode import u + + +def _override_from(config, override_varname, key=None): + try: + overrides = vim_getvar(override_varname) + except KeyError: + return config + if key is not None: + try: + overrides = overrides[key] + except KeyError: + return config + mergedicts(config, overrides) + return config + + +class VimVarHandler(logging.Handler, object): + '''Vim-specific handler which emits messages to Vim global variables + + :param str varname: + Variable where + ''' + def __init__(self, varname): + super(VimVarHandler, self).__init__() + utf_varname = u(varname) + self.vim_varname = utf_varname.encode('ascii') + vim.command('unlet! g:' + utf_varname) + vim.command('let g:' + utf_varname + ' = []') + + def emit(self, record): + message = u(record.message) + if record.exc_text: + message += '\n' + u(record.exc_text) + vim.eval(b'add(g:' + self.vim_varname + b', ' + python_to_vim(message) + b')') + + +class VimPowerline(Powerline): + def init(self, pyeval='PowerlinePyeval', **kwargs): + super(VimPowerline, self).init('vim', **kwargs) + self.last_window_id = 1 + self.pyeval = pyeval + self.construct_window_statusline = self.create_window_statusline_constructor() + if all((hasattr(vim.current.window, attr) for attr in ('options', 'vars', 'number'))): + self.win_idx = self.new_win_idx + else: + self.win_idx = self.old_win_idx + self._vim_getwinvar = vim_get_func('getwinvar', 'bytes') + self._vim_setwinvar = vim_get_func('setwinvar') + + if sys.version_info < (3,): + def create_window_statusline_constructor(self): + window_statusline = b'%!' + str(self.pyeval) + b'(\'powerline.statusline({0})\')' + return window_statusline.format + else: + def create_window_statusline_constructor(self): + startstr = b'%!' + self.pyeval.encode('ascii') + b'(\'powerline.statusline(' + endstr = b')\')' + return lambda idx: ( + startstr + str(idx).encode('ascii') + endstr + ) + + create_window_statusline_constructor.__doc__ = ( + '''Create function which returns &l:stl value being given window index + + Created function must return :py:class:`bytes` instance because this is + what ``window.options['statusline']`` returns (``window`` is + :py:class:`vim.Window` instance). + + :return: + Function with type ``int → bytes``. + ''' + ) + + default_log_stream = sys.stdout + + def add_local_theme(self, key, config): + '''Add local themes at runtime (during vim session). + + :param str key: + Matcher name (in format ``{matcher_module}.{module_attribute}`` or + ``{module_attribute}`` if ``{matcher_module}`` is + ``powerline.matchers.vim``). Function pointed by + ``{module_attribute}`` should be hashable and accept a dictionary + with information about current buffer and return boolean value + indicating whether current window matched conditions. See also + :ref:`local_themes key description <config-ext-local_themes>`. + + :param dict config: + :ref:`Theme <config-themes>` dictionary. + + :return: + ``True`` if theme was added successfully and ``False`` if theme with + the same matcher already exists. + ''' + self.update_renderer() + matcher = self.get_matcher(key) + theme_config = {} + for cfg_path in self.theme_levels: + try: + lvl_config = self.load_config(cfg_path, 'theme') + except IOError: + pass + else: + mergedicts(theme_config, lvl_config) + mergedicts(theme_config, config) + try: + self.renderer.add_local_theme(matcher, {'config': theme_config}) + except KeyError: + return False + else: + # Hack for local themes support: when reloading modules it is not + # guaranteed that .add_local_theme will be called once again, so + # this function arguments will be saved here for calling from + # .do_setup(). + self.setup_kwargs.setdefault('_local_themes', []).append((key, config)) + return True + + get_encoding = staticmethod(get_vim_encoding) + + def load_main_config(self): + main_config = _override_from(super(VimPowerline, self).load_main_config(), 'powerline_config_overrides') + try: + use_var_handler = bool(int(vim_getvar('powerline_use_var_handler'))) + except KeyError: + use_var_handler = False + if use_var_handler: + main_config.setdefault('common', {}) + main_config['common'] = finish_common_config(self.get_encoding(), main_config['common']) + main_config['common']['log_file'].append(['powerline.vim.VimVarHandler', [['powerline_log_messages']]]) + return main_config + + def load_theme_config(self, name): + return _override_from( + super(VimPowerline, self).load_theme_config(name), + 'powerline_theme_overrides', + name + ) + + def get_local_themes(self, local_themes): + if not local_themes: + return {} + + return dict(( + (matcher, {'config': self.load_theme_config(val)}) + for matcher, key, val in ( + ( + (None if k == '__tabline__' else self.get_matcher(k)), + k, + v + ) + for k, v in local_themes.items() + ) if ( + matcher or + key == '__tabline__' + ) + )) + + def get_matcher(self, match_name): + match_module, separator, match_function = match_name.rpartition('.') + if not separator: + match_module = 'powerline.matchers.{0}'.format(self.ext) + match_function = match_name + return self.get_module_attr(match_module, match_function, prefix='matcher_generator') + + def get_config_paths(self): + try: + return vim_getvar('powerline_config_paths') + except KeyError: + return super(VimPowerline, self).get_config_paths() + + def do_setup(self, pyeval=None, pycmd=None, can_replace_pyeval=True, _local_themes=()): + import __main__ + if not pyeval: + pyeval = 'pyeval' if sys.version_info < (3,) else 'py3eval' + can_replace_pyeval = True + if not pycmd: + pycmd = get_default_pycmd() + + set_pycmd(pycmd) + + # pyeval() and vim.bindeval were both introduced in one patch + if (not hasattr(vim, 'bindeval') and can_replace_pyeval) or pyeval == 'PowerlinePyeval': + vim.command((''' + function! PowerlinePyeval(e) + {pycmd} powerline.do_pyeval() + endfunction + ''').format(pycmd=pycmd)) + pyeval = 'PowerlinePyeval' + + self.pyeval = pyeval + self.construct_window_statusline = self.create_window_statusline_constructor() + + self.update_renderer() + __main__.powerline = self + + try: + if ( + bool(int(vim.eval('has(\'gui_running\') && argc() == 0'))) + and not vim.current.buffer.name + and len(vim.windows) == 1 + ): + # Hack to show startup screen. Problems in GUI: + # - Defining local value of &statusline option while computing + # global value purges startup screen. + # - Defining highlight group while computing statusline purges + # startup screen. + # This hack removes the “while computing statusline” part: both + # things are defined, but they are defined right now. + # + # The above condition disables this hack if no GUI is running, + # Vim did not open any files and there is only one window. + # Without GUI everything works, in other cases startup screen is + # not shown. + self.new_window() + except UnicodeDecodeError: + # vim.current.buffer.name may raise UnicodeDecodeError when using + # Python-3*. Fortunately, this means that current buffer is not + # empty buffer, so the above condition should be False. + pass + + # Cannot have this in one line due to weird newline handling (in :execute + # context newline is considered part of the command in just the same cases + # when bar is considered part of the command (unless defining function + # inside :execute)). vim.command is :execute equivalent regarding this case. + vim.command('augroup Powerline') + vim.command(' autocmd! ColorScheme * :{pycmd} powerline.reset_highlight()'.format(pycmd=pycmd)) + vim.command(' autocmd! VimLeavePre * :{pycmd} powerline.shutdown()'.format(pycmd=pycmd)) + vim.command('augroup END') + + # Hack for local themes support after reloading. + for args in _local_themes: + self.add_local_theme(*args) + + def reset_highlight(self): + try: + self.renderer.reset_highlight() + except AttributeError: + # Renderer object appears only after first `.render()` call. Thus if + # ColorScheme event happens before statusline is drawn for the first + # time AttributeError will be thrown for the self.renderer. It is + # fine to ignore it: no renderer == no colors to reset == no need to + # do anything. + pass + + def new_win_idx(self, window_id): + r = None + + for window in vim.windows: + try: + curwindow_id = window.vars['powerline_window_id'] + if r is not None and curwindow_id == window_id: + raise KeyError + except KeyError: + curwindow_id = self.last_window_id + self.last_window_id += 1 + window.vars['powerline_window_id'] = curwindow_id + statusline = self.construct_window_statusline(curwindow_id) + if window.options['statusline'] != statusline: + window.options['statusline'] = statusline + if curwindow_id == window_id if window_id else window is vim.current.window: + r = (window, curwindow_id, window.number) + return r + + def old_win_idx(self, window_id): + r = None + for winnr, window in zip(count(1), vim.windows): + curwindow_id = self._vim_getwinvar(winnr, 'powerline_window_id') + if curwindow_id and not (r is not None and curwindow_id == window_id): + curwindow_id = int(curwindow_id) + else: + curwindow_id = self.last_window_id + self.last_window_id += 1 + self._vim_setwinvar(winnr, 'powerline_window_id', curwindow_id) + statusline = self.construct_window_statusline(curwindow_id) + if self._vim_getwinvar(winnr, '&statusline') != statusline: + self._vim_setwinvar(winnr, '&statusline', statusline) + if curwindow_id == window_id if window_id else window is vim.current.window: + r = (window, curwindow_id, winnr) + return r + + def statusline(self, window_id): + window, window_id, winnr = self.win_idx(window_id) or (None, None, None) + if not window: + return FailedUnicode('No window {0}'.format(window_id)) + return self.render(window, window_id, winnr) + + def tabline(self): + r = self.win_idx(None) + + if r: + return self.render(*r, is_tabline=True) + else: + win = vim.current.window + r = ( + win, + win.vars.get('powerline_window_id', self.last_window_id), + win.number, + ) + return self.render(*r, is_tabline=True) + + def new_window(self): + return self.render(*self.win_idx(None)) + + @staticmethod + def do_pyeval(): + '''Evaluate python string passed to PowerlinePyeval + + Is here to reduce the number of requirements to __main__ globals to just + one powerline object (previously it required as well vim and json). + ''' + import __main__ + vim.command('return ' + json.dumps(eval(vim.eval('a:e'), __main__.__dict__))) + + def setup_components(self, components): + if components is None: + components = ('statusline', 'tabline') + if 'statusline' in components: + # Is immediately changed after new_window function is run. Good for + # global value. + vim.command('set statusline=%!{pyeval}(\'powerline.new_window()\')'.format( + pyeval=self.pyeval)) + if 'tabline' in components: + vim.command('set tabline=%!{pyeval}(\'powerline.tabline()\')'.format( + pyeval=self.pyeval)) + + +pycmd = None + + +def set_pycmd(new_pycmd): + global pycmd + pycmd = new_pycmd + + +def get_default_pycmd(): + return 'python' if sys.version_info < (3,) else 'python3' + + +def setup(*args, **kwargs): + powerline = VimPowerline() + return powerline.setup(*args, **kwargs) |