Source code for vimtk.core

"""
The core API to the vimtk Python module

SeeAlso:
    ./__init__.py
    ../autoload/vimtk.vim

"""
import itertools as it
from os.path import join
from os.path import isdir
from os.path import exists
from os.path import expanduser
import re
import sys
import pathlib
import logging
from vimtk import util
from vimtk.util import (
    dict_union, ensure_unicode, indent, codeblock, group_items, expandpath)

try:
    import ubelt as ub
except Exception:
    print('Warning: Unable to import ubelt')
    print('Note: vim has inconsistent interactions with which python it chooses')
    print('\nsys.prefix = {}\n'.format(sys.prefix))
    _sys_exe = pathlib.Path(sys.executable)
    _real_exe = _sys_exe.resolve()
    if _sys_exe == _real_exe:
        print('\nsys.executable = {}  # not a symlink\n'.format(_sys_exe))
    else:
        print('\nsys.executable = {} -> {}\n'.format(_sys_exe, _real_exe))
    print('\nsys.path = {}\n'.format(sys.path))
    ub = None
    # raise

from vimtk import xctrl
from vimtk import cplat
import sys

logger = logging.getLogger(__name__)


WIN32   = sys.platform == 'win32'  # type: bool


[docs] def __setup_logger(): # TODO: setting up logging should be handled by the vim plugin # global loggers in python modules should not log anywhere by default global logger # logger.propagate = False # Remove existing handlers for h in list(logger.handlers): logger.removeHandler(h) # By default loggers write to stderr, but we want to write to stdout # so vim doesn't interpret logs as errors fmtr = logging.Formatter('%(levelname)s: %(message)s') hdlr = logging.StreamHandler(sys.stdout) hdlr.setFormatter(fmtr) hdlr.setLevel(logging.INFO) # hdlr.setLevel(logging.DEBUG) logger.addHandler(hdlr)
__setup_logger() # logger.basicConfig() # logger.setLevel(logging.INFO) # logger.setLevel(logging.DEBUG) __docstubs__ = """ import vimtk._demo.vimmock """
[docs] def mockvim(fpath=None, text=None): """ Setup a dummy "vim" module with a specific buffer Useful for prototyping new commands and testing Args: fpath (PathLike | None): File to mock open. Will read the text if it exists. text (str | None): text of the mock buffer. If none, tries to read from the file path. Returns: vimtk._demo.vimmock.mocked.VimMock: the mock vim module Example: >>> import vimtk >>> vim = vimtk.mockvim() >>> # The mock mirrors the vim module as best as it can >>> print('vim.current.buffer = {}'.format(vim.current.buffer)) >>> print('vim.current.buffer.name = {}'.format(vim.current.buffer.name)) >>> vim.eval("let g:custom_global = 'custom_val'") >>> value = vim.eval('get(g:, "custom_global")') >>> assert value == 'custom_val' """ import os from vimtk._demo import vimmock vim = vimmock.patch_vim() if text is not None: if fpath is None: fpath = '' vim.setup_text(text, name=os.fspath(fpath)) elif fpath is not None: vim.open_file(fpath) return vim
[docs] def reload_vimtk(): """ Used for development """ try: import importlib reload = importlib.reload except (AttributeError, ImportError): import imp reload = imp.reload logger.debug('Reloading vimtk') import vimtk import vimtk.core import vimtk.xctrl import vimtk.pyinspect import vimtk.cplat reload(vimtk.pyinspect) reload(vimtk.cplat) reload(vimtk.core) reload(vimtk.xctrl) reload(vimtk) if WIN32: import vimtk.win32_ctrl reload(vimtk.win32_ctrl)
reload = reload_vimtk
[docs] class Config(object): """ Query the state of the vim variable namespace. Notes: >>> import vimtk >>> vim = vimtk.mockvim() >>> vim.eval("let g:vimtk_sys_path = ['$HOME/code/netharn']") >>> vimtk.CONFIG.get('vimtk_sys_path') >>> vimtk.CONFIG['vimtk_auto_importable_modules'] >>> # Should the vim variable override or update the default config? >>> vim.eval("let g:vimtk_auto_importable_modules = {'spam': 'import spam'}") >>> vimtk.CONFIG['vimtk_auto_importable_modules'] """ def __init__(self): # TODO: use scriptconfig to add helps? self.default = { 'vimtk_terminal_pattern': None, 'vimtk_multiline_num_press_enter': 3, 'vimtk_auto_importable_modules': { 'it': 'import itertools as it', 'nh': 'import netharn as nh', 'np': 'import numpy as np', 'pd': 'import pandas as pd', 'ub': 'import ubelt as ub', 'nx': 'import networkx as nx', 'Image': 'from PIL import Image', 'mpl': 'import matplotlib as mpl', 'nn': 'from torch import nn', 'torch_data': 'import torch.utils.data as torch_data', 'F': 'import torch.nn.functional as F', 'math': 'import math', }, # Additional paths to search when resolving python modnames 'vimtk_sys_path': [], } self.state = self.default.copy() pass def __getitem__(self, key): value = self.get(key, default=self.state[key]) return value def __setitem__(self, key, value): self.state[key] = value
[docs] def get(self, key, default=None, context='g'): """ gets the value of a vim variable and defaults if it does not exist """ try: import vim except ImportError: return default assert key in self.default varname = '{}:{}'.format(context, key) var_exists = int(vim.eval('exists("{}")'.format(varname))) if var_exists: value = vim.eval('get({}:, "{}")'.format(context, key)) # Hack: for dictionaries, update instead of overriding? # Not sure if this is a good idea if isinstance(value, dict): value = dict_union(default, value) else: value = default return value
[docs] class Clipboard(object):
[docs] @staticmethod def copy(text): return cplat.copy_text_to_clipboard(text)
[docs] @staticmethod def paste(): # Using pyperclip seems to freeze. # Access clipboard via vim instead try: import vim text = vim.eval('@+') except ImportError: text = cplat.get_clipboard() return text
[docs] class TextSelector(object): r""" Tools for selecting and reading text from Vim """
[docs] @staticmethod def current_indent(url_ok=False): """ Returns the indentation that should be used when inserting new lines. Example: >>> import vimtk >>> vim = vimtk.mockvim(text=codeblock( >>> ''' >>> class Foo: >>> def __init__(self): >>> self.foo = 1 >>> self.foo = 2 >>> ''')) >>> vim.move_cursor(1) >>> n1 = len(vimtk.TextSelector.current_indent()) >>> vim.move_cursor(2) >>> n2 = len(vimtk.TextSelector.current_indent()) >>> vim.move_cursor(3) >>> n3 = len(vimtk.TextSelector.current_indent()) >>> assert (n1, n2, n3) == (4, 8, 8) """ # Check current line for cues curr_line = TextSelector.line_at_cursor() curr_indent = get_minimum_indentation(curr_line) if curr_line is None: next_line = '' if curr_line.strip().endswith(':'): curr_indent += 4 # Check next line for cues next_line = get_first_nonempty_line_after_cursor() if next_line is None: next_line = '' next_indent = get_minimum_indentation(next_line) if next_indent <= curr_indent + 8: # hack for overindented lines min_indent = max(curr_indent, next_indent) else: min_indent = curr_indent indent = (' ' * min_indent) if curr_line.strip().startswith('>>>'): indent += '>>> ' return indent
[docs] @staticmethod def word_at_cursor(url_ok=False): """ returns the word highlighted by the curor Example: >>> import vimtk >>> vim = vimtk.mockvim() >>> vim.setup_text(codeblock( >>> ''' >>> class Foo: >>> def __init__(self): >>> self.foo = 1 >>> self.bar = 2 >>> ''')) >>> vim.move_cursor(3, 14) >>> word = TextSelector.word_at_cursor() >>> print('word = {!r}'.format(word)) word = 'self.foo' """ import vim buf = vim.current.buffer (row, col) = vim.current.window.cursor line = buf[row - 1] # Original end of the file if url_ok: nonword_chars_left = ' \t\n\r{},"\'\\' nonword_chars_right = nonword_chars_left else: nonword_chars_left = ' \t\n\r[](){}:;,"\'\\/=$*' nonword_chars_right = ' \t\n\r[](){}:;,"\'\\/=$*.' word = TextSelector.get_word_in_line_at_col( line, col, nonword_chars_left=nonword_chars_left, nonword_chars_right=nonword_chars_right) return word
[docs] @staticmethod def get_word_in_line_at_col(line, col, nonword_chars_left=' \t\n\r[](){}:;,"\'\\/', nonword_chars_right=None): r""" Args: line (str): col (int): CommandLine: python -m vimtk.core TextSelector.get_word_in_line_at_col Example: >>> import vimtk >>> line = 'myvar.foo = yourvar.foobar' >>> line = 'def loadfunc(self):' >>> col = 6 >>> nonword_chars=' \t\n\r[](){}:;.,"\'\\/' >>> word = vimtk.TextSelector.get_word_in_line_at_col(line, col, nonword_chars) >>> result = ('word = %r' % (word,)) >>> print(result) """ if nonword_chars_right is None: nonword_chars_right = nonword_chars_left lpos = col rpos = col while lpos > 0: # Expand to the left if line[lpos] in nonword_chars_left: lpos += 1 break lpos -= 1 while rpos < len(line): # Expand to the right if line[rpos] in nonword_chars_right: break rpos += 1 word = line[lpos:rpos] return word
[docs] @staticmethod def selected_text(select_at_cursor=False): r""" Returns all text in curent selection. make sure the vim function calling this has a range after () Currently used by <ctrl+g> Refered to [vim_between_selection]_. References: .. [vim_between_selection] http://stackoverflow.com/questions/18165973/vim-between-selection SeeAlso: ~/local/vim/rc/custom_misc_functions.vim Test paragraph. Far out in the uncharted backwaters of the unfashionable end of the western spiral arm of the Galaxy lies a small unregarded yellow sun. Orbiting this at a distance of roughly ninety-two million miles is an utterly insignificant little blue green planet whose ape-descended life forms are so amazingly primitive that they still think digital watches are a pretty neat idea. % --- one. two three. four. CommandLine: xdoctest -m vimtk.core TextSelector.selected_text Example: >>> import vimtk >>> vim = vimtk.mockvim() >>> vim.setup_text(codeblock( >>> ''' >>> line n1 >>> line n2 >>> line n3 >>> line n4 >>> ''')) >>> vim.move_cursor(3) >>> vim.current.buffer._visual_select(2, 3) >>> text = TextSelector.selected_text() >>> print(text) line n2 line n3 >>> vim.current.buffer._visual_select(2, 3, 0, 5) >>> text = TextSelector.selected_text() >>> print(text) line n line n """ import vim logger.debug('grabbing visually selected text') buf = vim.current.buffer pos1 = buf.mark('<') pos2 = buf.mark('>') if pos1 and pos2: (lnum1, col1) = pos1 (lnum2, col2) = pos2 text = TextSelector.text_between_lines(lnum1, lnum2, col1, col2) return text
[docs] @staticmethod def text_between_lines(lnum1, lnum2, col1=0, col2=sys.maxsize - 1): """ """ import vim # lines = vim.eval('getline({}, {})'.format(lnum1, lnum2)) lines = vim.current.buffer[lnum1 - 1:lnum2] lines = [ensure_unicode(line) for line in lines] try: if len(lines) == 0: pass elif len(lines) == 1: lines[0] = lines[0][col1:col2 + 1] else: # lines[0] = lines[0][col1:] # lines[-1] = lines[-1][:col2 + 1] for i in range(len(lines)): lines[i] = lines[i][col1:col2 + 1] text = '\n'.join(lines) except Exception: print(repr(lines)) raise return text
[docs] @staticmethod def line_at_cursor(): """ Example: >>> import vimtk >>> vim = vimtk.mockvim() >>> vim.setup_text(codeblock( >>> ''' >>> def foo(): >>> return 1 >>> def bar(): >>> return 2 >>> ''')) >>> vim.move_cursor(3) >>> line = vimtk.TextSelector.line_at_cursor() >>> assert line == 'def bar():' """ import vim logger.debug('grabbing text at current line') buf = vim.current.buffer (row, col) = vim.current.window.cursor line = buf[row - 1] return line
[docs] @staticmethod def paragraph_range_at_cursor(): r""" Get the start and end lines for a paragraph at the cursor Example: >>> import vimtk >>> vim = vimtk.mockvim() >>> text = codeblock( ''' par1 par1 par1 par1 par1 par1 par1 par2 par3 par3 par3 ''') >>> vim.setup_text(text) >>> ranges = [] >>> for lineno in range(1, text.count(chr(10)) + 1): >>> vim.move_cursor(lineno) >>> par_range = vimtk.TextSelector.paragraph_range_at_cursor() >>> ranges.append(par_range) >>> import ubelt as ub >>> print('ranges = {}'.format(ub.urepr(ranges, nl=0))) ranges = [(0, 3), (0, 3), (0, 3), (4, 4), (5, 5), (6, 6), (7, 7)] """ import vim logger.debug('grabbing text at current line') def is_paragraph_end(line_): # Hack, par_marker_list should be an argument striped_line = ensure_unicode(line_.strip()) isblank = striped_line == '' if isblank: return True # TODO: fixme, move to some configuration file par_marker_list = [ #'\\noindent', '\\begin{equation}', '\\end{equation}', '% ---', ] return any(striped_line.startswith(marker) for marker in par_marker_list) def find_paragraph_end(row_, direction=1): """ returns the line that a paragraph ends on in some direction """ # TODO: validate logic. line_list = vim.current.buffer line_ = line_list[row_ - 1] if (row_ == 0 or row_ == len(line_list) - 1): return row_ if is_paragraph_end(line_): return row_ while True: if (row_ == -1 or row_ == len(line_list)): break line_ = line_list[row_ - 1] if is_paragraph_end(line_): break row_ += direction row_ -= direction return row_ (row, col) = vim.current.window.cursor row1 = find_paragraph_end(row, -1) row2 = find_paragraph_end(row, +1) # row1 = max(1, row1) par_range = (row1, row2) return par_range
[docs] class CursorContext(object): """ moves back to original position after context is done """ def __init__(self, offset=0): self.pos = None self.offset = offset def __enter__(self): self.pos = Cursor.position() return self def __exit__(self, *exc_info): row, col = self.pos row += self.offset Cursor.move(row, col)
[docs] class Cursor(object):
[docs] @staticmethod def move(row, col=0): """ move_cursor """ import vim vim.command('cal cursor({},{})'.format(row, col))
[docs] @staticmethod def position(): """ get_cursor_position """ import vim # This is 1 indexed, should we change that? (row, col) = vim.current.window.cursor return row, col
[docs] class TextInsertor(object): """ Tools for inserting text at various positions """
[docs] def overwrite(text): """ Overwrites all existing text in the current buffer with new text Example: >>> import vimtk >>> vim = vimtk.mockvim(text='foo') >>> vimtk.TextInsertor.overwrite('bar') >>> print(vim.current.buffer._text) """ import vim lines = text.split('\n') vim.current.buffer[:] = lines
# del (vim.current.buffer[:]) # vim.current.buffer.append(lines)
[docs] @staticmethod def insert_at(text, pos): import vim # lines = [line.encode('utf-8') for line in text.split('\n')] lines = [line for line in text.split('\n')] buffer_tail = vim.current.buffer[pos:] # Original end of the file new_tail = lines + buffer_tail # Prepend our data del vim.current.buffer[pos:] # delete old data vim.current.buffer.append(new_tail) # extend new data
[docs] @staticmethod def insert_under_cursor(text): """ Example: >>> import vimtk >>> vim = vimtk.mockvim(text='foo') >>> vimtk.TextInsertor.insert_under_cursor('bar') >>> print(vim.current.buffer._text) foo bar """ import vim (row, col) = vim.current.window.cursor # Rows are 1 indexed, so no need to increment TextInsertor.insert_at(text, row)
[docs] @staticmethod def insert_above_cursor(text): """ Example: >>> import vimtk >>> vim = vimtk.mockvim() >>> vim.setup_text('foo') >>> vimtk.TextInsertor.insert_above_cursor('bar') >>> print(vim.current.buffer._text) bar foo """ import vim (row, col) = vim.current.window.cursor row = row - 1 TextInsertor.insert_at(text, row)
[docs] @staticmethod def insert_over_selection(text): import vim buf = vim.current.buffer # These are probably 1 based (row1, col1) = buf.mark('<') (row2, col2) = buf.mark('>') TextInsertor.insert_between_lines(text, row1, row2)
[docs] @staticmethod def insert_between_lines(text, row1, row2): import vim # print(f'text={text}') # print(f'Insert between {row1} {row2}') buffer_head = vim.current.buffer[:row1 - 1] # Original start of the file buffer_tail = vim.current.buffer[row2:] # Original end of the file # print(f'buffer_tail={buffer_tail}') lines = [line.encode('utf-8') for line in text.split('\n')] new_buffer = buffer_head + lines + buffer_tail vim.current.buffer[:] = new_buffer
# del vim.current.buffer[row1 - 1:] # delete old data # vim.current.buffer.append(new_tail) # append new data
[docs] class Mode(object): """ Helper for checking / switching modes """ vim_mode_codes = { 'n' : 'Normal', 'no' : 'NOperatorPending', 'v' : 'Visual', 'V' : 'VLine', #'^V' : 'VBlock', 's' : 'Select', 'S' : 'SLine', #'^S' : 'SBlock', 'i' : 'Insert', 'R' : 'Replace', 'Rv' : 'VReplace', 'c' : 'Command', 'cv' : 'VimEx', 'ce' : 'Ex', 'r' : 'Prompt', 'rm' : 'More', 'r?' : 'Confirm', '!' : 'Shell', }
[docs] @staticmethod def current(): """ Return the current mode References: http://stackoverflow.com/questions/14013294/vim-how-to-detect-the-mode-in-which-the-user-is-in-for-statusline Example: >>> import vimtk >>> vim = vimtk.mockvim() >>> vimtk.Mode.current() Normal """ import vim current_mode_code = vim.eval('mode()') current_mode = Mode.vim_mode_codes.get(current_mode_code, current_mode_code) return current_mode
[docs] @staticmethod def ensure_normal(): """ Switch to normal mode Example: >>> import vimtk >>> vim = vimtk.mockvim() >>> vimtk.Mode.ensure_normal() >>> vimtk.Mode.current() Normal """ current_mode = Mode.current() import vim if current_mode == 'Normal': return else: logger.debug('current_mode_code = %r' % current_mode) logger.debug('current_mode = %r' % current_mode) vim.command("ESC")
[docs] class Python(object): """ Tools for handling python-specific functions """
[docs] @staticmethod def current_module_info(): """ Returns information about current module Example: >>> import vimtk >>> vim = vimtk.mockvim('foo.py', '') >>> info = vimtk.Python.current_module_info() >>> print(info) """ import vim modpath = vim.current.buffer.name modname = util.modpath_to_modname(modpath, check=False) moddir, rel_modpath = util.split_modpath(modpath, check=False) from ubelt.util_import import is_modname_importable importable = is_modname_importable(modname, exclude=['.']) info = { 'modname': modname, 'modpath': modpath, 'importable': importable, } return info
[docs] @staticmethod def is_module_pythonfile(): from os.path import splitext import vim modpath = vim.current.buffer.name ext = splitext(modpath)[1] ispyfile = ext == '.py' logger.debug('is_module_pythonfile?') logger.debug(' * modpath = %r' % (modpath,)) logger.debug(' * ext = %r' % (ext,)) logger.debug(' * ispyfile = %r' % (ispyfile,)) return ispyfile
[docs] @staticmethod def find_import_row(): """ Find lines where import block begins (after __future__) """ in_comment = False import vim row = 0 # FIXME: currently heuristic based for row, line in enumerate(vim.current.buffer): if not in_comment: if line.strip().startswith('#'): pass elif line.strip().startswith('"""'): in_comment = '"' elif line.strip().startswith("''''"): in_comment = "'" elif line.startswith('from __future__'): pass elif line.startswith('import'): break elif line.startswith('from'): break else: break else: if line.strip().endswith(in_comment * 3): in_comment = False return row
[docs] @staticmethod def prepend_import_block(text): import vim row = Python.find_import_row() # FIXME: doesnt work right when row=0 buffer_tail = vim.current.buffer[row:] lines = [line.encode('utf-8') for line in text.split('\n')] lines = [x for x in lines if x] logger.info('lines = {!r}'.format(lines)) new_tail = lines + buffer_tail del vim.current.buffer[row:] # delete old data # vim's buffer __del__ method seems to not work when the slice is 0:None. # It should remove everything, but it seems that one item still exists # It seems we can never remove that last item, so we have to hack. hackaway_row0 = row == 0 and len(vim.current.buffer) == 1 # print(len(vim.current.buffer)) # print('vim.current.buffer = {!r}'.format(vim.current.buffer[:])) vim.current.buffer.append(new_tail) # append new data if hackaway_row0: del vim.current.buffer[0]
[docs] @staticmethod def format_text_as_docstr(text): r""" CommandLine: python ~/local/vim/rc/pyvim_funcs.py --test-format_text_as_docstr Example: >>> import vimtk >>> text = codeblock( ''' a = 1 b = 2 ''') >>> formated_text = vimtk.Python.format_text_as_docstr(text) >>> unformated_text = vimtk.Python.unformat_text_as_docstr(formated_text) >>> print(formated_text) >>> print(unformated_text) """ min_indent = get_minimum_indentation(text) indent_ = ' ' * min_indent formated_text = re.sub('^' + indent_, '' + indent_ + '>>> ', text, flags=re.MULTILINE) formated_text = re.sub('^$', '' + indent_ + '>>> #', formated_text, flags=re.MULTILINE) return formated_text
[docs] @staticmethod def unformat_text_as_docstr(formated_text): min_indent = get_minimum_indentation(formated_text) indent_ = ' ' * min_indent unformated_text = re.sub('^' + indent_ + '>>> ', '' + indent_, formated_text, flags=re.MULTILINE) return unformated_text
[docs] @staticmethod def find_func_above_row(row='current', maxIter=50): """ Example: >>> import vimtk >>> vim = vimtk.mockvim() >>> vim.setup_text(codeblock( >>> ''' >>> class Foo: >>> def __init__(self): >>> self.foo = 1 >>> self.foo = 2 >>> def foo(): >>> ... >>> def bar(): >>> ... >>> def baz(): >>> ... >>> class Biz: >>> def __init__(self): >>> self.foo = 1 >>> self.foo = 2 >>> def buzz(self): >>> ... >>> ''')) >>> vim.move_cursor(8) >>> info = vimtk.Python.find_func_above_row() >>> import ubelt as ub >>> print(ub.urepr(info)) { 'callname': 'bar', 'pos': 6, 'line': 'def bar():', } >>> vim.move_cursor(4) >>> info = vimtk.Python.find_func_above_row() >>> print(ub.urepr(info)) { 'callname': 'Foo.__init__', 'pos': 1, 'line': ' def __init__(self):', } >>> vim.move_cursor(16) >>> info = vimtk.Python.find_func_above_row() >>> print(ub.urepr(info)) { 'callname': 'Biz.buzz', 'pos': 14, 'line': ' def buzz(self):', } Ignore: import xdev b = xdev.RegexBuilder.coerce('python') parts = [ b.named_field(b.whitespace, 'indent'), b.named_field('def|class', 'type'), b.whitespace, b.named_field(b.identifier, 'callname'), ] pattern = ''.join(parts) print(f"pattern = r'{pattern}'") """ import vim pattern = r'(?P<indent>\s*)(?P<type>def|class)\s*(?P<callname>[A-Za-z_][A-Za-z_0-9]*)' row = vim.current.window.cursor[0] - 1 line_list = vim.current.buffer result = find_pattern_above_row(pattern, line_list, row, maxIter=maxIter) if result is not None: line, pos, found = result groups = found.groupdict() callname = groups['callname'] if len(groups['indent']) > 0: # Probably part of a class. Try to find the class name. pattern = r'(?P<indent>\s*)(?P<type>class)\s*(?P<callname>[A-Za-z_][A-Za-z_0-9]*)' result2 = find_pattern_above_row(pattern, line_list, pos, maxIter=maxIter * 100) if result2 is not None: found2 = result2[-1] groups2 = found2.groupdict() callname = groups2['callname'] + '.' + callname else: line = None pos = None callname = None info = { 'callname': callname, 'pos': pos, 'line': line, } return info
[docs] @staticmethod def _convert_dicts_to_literals(text): """ TODO: where does this belong? This is a Python reformater of sorts. Example: import vimtk vim = text=codeblock( ''' data = dict( key1=12321321, key2='24324324', key3=myfunc(), key4=[ 1, 2, 3, 4, dict(a='b'), ] ) ''') new_text = vimtk.Python._convert_dicts_to_literals(text) print(new_text) """ # TODO: probably want a CST transformer instead import ast import astunparse class RewriteDictAsLiteral(ast.NodeTransformer): def visit_Call(self, node): if node.func.id == 'dict': keys = [ast.Constant(kw.arg) for kw in node.keywords] values = [kw.value for kw in node.keywords] literal = ast.Dict(keys=keys, values=values) return self.visit(literal) lvl = get_minimum_indentation(text) orig_ptree = ast.parse(codeblock(text)) xform_ptree = RewriteDictAsLiteral().visit(orig_ptree) xform_text = astunparse.unparse(xform_ptree) import black xform_text = black.format_str( xform_text, mode=black.Mode(string_normalization=False) ) new_text = indent(xform_text, ' ' * lvl) return new_text
[docs] @staticmethod def _convert_selection_to_literal_dict(): """ Changes the visual selection from a programatic dictionary to a dictionary literal if possible. Ignore: import vimtk vim = vimtk.mockvim(text=codeblock( ''' data = dict( key1=12321321, key2='24324324', key3=myfunc(), key4=[ 1, 2, 3, 4, dict(a='b'), ] ) ''')) vim.current.buffer._visual_select(1, 9) vimtk.Python._convert_selection_to_literal_dict() print(vimtk.TextSelector.selected_text()) """ import vimtk text = vimtk.TextSelector.selected_text() new_text = vimtk.Python._convert_dicts_to_literals(text) vimtk.TextInsertor.insert_over_selection(new_text)
[docs] def sys_executable(): """ Find the system executable. For whatever reason, vim messes with it. References: https://github.com/ycm-core/YouCompleteMe/blob/ba7a9f07a57c657c684edb5dde1f1f1dda1c0c7a/python/ycm/paths.py https://github.com/davidhalter/jedi-vim/issues/870 """ if sys.platform.startswith('win32'): executable = join(sys.exec_prefix, 'python.exe') else: import os bin_dpath = join(sys.exec_prefix, 'bin') assert exists(bin_dpath) pyexe_re = re.compile(r'python([23](\.[5-9])?)?(.exe)?$', re.IGNORECASE ) candiates = os.listdir(bin_dpath) found = [] for cand in candiates: if pyexe_re.match(cand): fpath = join(bin_dpath, cand) if os.path.isfile(fpath): if os.access(fpath, os.X_OK): found.append(fpath) assert len(found) > 0 executable = found[0] return executable
[docs] def preprocess_executable_text(text): """ Handles the case where we are trying to docstrings paste into IPython. """ import textwrap logger.debug('preprocesing executable text') # Prepare to send text to xdotool text = textwrap.dedent(text) # Preprocess the strings a bit lines = text.splitlines(True) # Handle C++ pybind11 docs if all(re.match(r'" *(>>>)|(\.\.\.) .*', line) for line in lines): if all(line.strip().endswith('"') for line in lines): new_lines = [] for line in lines: if line.endswith('\\n"\n'): line = line[1:-4] + '\n' elif line.endswith('"\n'): line = line[1:-2] + '\n' elif line.endswith('\\n"'): line = line[1:-3] elif line.endswith('"'): line = line[1:-1] else: raise AssertionError('unknown case') new_lines.append(line) lines = new_lines text = textwrap.dedent(''.join(lines)) lines = text.splitlines(True) # Strip docstring prefix if all(line.startswith(('>>> ', '...')) for line in lines): lines = [line[4:] for line in lines] text = ''.join(lines) text = textwrap.dedent(text) return text
[docs] def execute_text_in_terminal(text, return_to_vim=True): """ Execute the current text currently selected **vim** text The steps taken: (1) Takes a block of text, (2) copies it to the clipboard, (3) finds the most recently used terminal, (4) pastes the text into the most recently used terminal, (5) presses enter (if needed), * to run what presumably is a command or script, (6) and then returns focus to vim. TODO: * If currently focused on a terminal, then focus in a different terminal! * User specified terminal pattern * User specified paste keypress * Allow usage from non-gui terminal vim. (ensure we can detect if we are running in a terminal and register our window as the active vim, and then paste into the second mru terminal) Ignore: from vimtk.core import execute_text_in_terminal execute_text_in_terminal('print("hello")') """ logger.debug('execute_text_in_terminal') # Copy the text to the clipboard Clipboard.copy(text) terminal_pattern = CONFIG.get('vimtk_terminal_pattern', None) vimtk_multiline_num_press_enter = CONFIG.get('vimtk_multiline_num_press_enter', 3) # Currently linux and windows are handled separately. It would be nice to # unify them if possible. if sys.platform.startswith('win32'): from vimtk import win32_ctrl import pywinauto active_gvim = win32_ctrl.find_window('gvim.exe') # TODO: custom terminal spec # Make sure regexes are bash escaped if terminal_pattern is None: terminal_pattern = '|'.join(map(re.escape, [ 'cmd.exe', 'Cmder', ])) terminal = win32_ctrl.find_window(terminal_pattern) terminal.focus() # TODO: some terminals paste with a right click on win32 # support these. if hasattr(pywinauto.keyboard, 'send_keys'): send_keys = pywinauto.keyboard.send_keys else: send_keys = pywinauto.keyboard.SendKeys send_keys('^v') send_keys('{ENTER}') send_keys('{ENTER}') if '\n' in text: for _ in range(vimtk_multiline_num_press_enter - 2): send_keys('{ENTER}') if return_to_vim: active_gvim.focus() else: if terminal_pattern is None: terminal_pattern = xctrl._wmctrl_terminal_patterns() # Sequence of key presses that will trigger a paste event paste_keypress = 'ctrl+shift+v' sleeptime = .01 import time time.sleep(.05) xctrl.XCtrl.cmd('xset r off') active_gvim = xctrl.XWindow.current() xctrl.XWindow.find(terminal_pattern).focus(sleeptime) xctrl.XCtrl.send_keys(paste_keypress, sleeptime) xctrl.XCtrl.send_keys('KP_Enter', sleeptime) # Need to time the enter key press correctly. # TODO: is there a better method to do this? time.sleep(0.1) xctrl.XCtrl.send_keys('KP_Enter', sleeptime) if '\n' in text: # Press enter multiple times for multiline texts for _ in range(vimtk_multiline_num_press_enter - 1): xctrl.XCtrl.send_keys('KP_Enter', sleeptime) if return_to_vim: active_gvim.focus(sleeptime) xctrl.XCtrl.cmd('xset r on')
[docs] def vim_argv(defaults=None): """ Helper for parsing vimscript function arguments Gets the arguments to the current variable args vim function Notes: For instance if you have a vim function .. code:: vim func! foo(...) echo "hi" python << EOF import vimtk # You could use this to extract what the args that it was # called with were. argv = vimtk.vim_argv() print('argv = {!r}'.format(argv)) EOF endfunc Example: >>> import vimtk >>> vim = vimtk.mockvim() >>> vim._push_function_stack(name='foo', positional=['val1', 'val2']) >>> argv = vimtk.vim_argv() >>> assert argv == ['val1', 'val2'] >>> argv = vimtk.vim_argv(defaults=['a', 'b', 'c']) >>> assert argv == ['val1', 'val2', 'c'] >>> _ = vim._function_stack.pop() """ import vim nargs = int(vim.eval('a:0')) argv = [vim.eval('a:{}'.format(i + 1)) for i in range(nargs)] if defaults is not None: # fill the remaining unspecified args with defaults n_remain = len(defaults) - len(argv) if n_remain > 0: remain = defaults[-n_remain:] argv += remain return argv
[docs] def get_current_fpath(): """ Example: >>> import vimtk >>> vim = vimtk.mockvim(fpath='foo.txt', text='') >>> fpath = vimtk.get_current_fpath() >>> assert fpath == 'foo.txt' """ import vim fpath = vim.current.buffer.name return fpath
[docs] def get_current_filetype(): """ Example: >>> import vimtk >>> vim = vimtk.mockvim(fpath='foo.sh', text='') >>> filetype = vimtk.get_current_filetype() >>> assert filetype == 'sh' """ import vim filetype = vim.eval('&ft') return filetype
[docs] def ensure_normalmode(): """ TODO: Deprecated in favor or Mode.ensure_normal() References: http://stackoverflow.com/questions/14013294/vim-how-to-detect-the-mode-in-which-the-user-is-in-for-statusline """ return Mode.ensure_normal()
[docs] def autogen_imports(fpath_or_text): """ Generate import statements for python code Example: >>> # xdoctest: +SKIP >>> # This test is broken on the windows CI and I dont understand why >>> import vimtk >>> source = codeblock( ''' math it ''') >>> text = vimtk.autogen_imports(source) ... >>> print(text) import itertools as it import math """ try: import xinspect except Exception: print('UNABLE TO IMPORT XINSPECT') print('sys.prefix = {!r}'.format(sys.prefix)) raise from os.path import exists from xinspect.autogen import Importables importable = Importables() importable._use_recommended_defaults() user_importable = None try: user_importable = CONFIG['vimtk_auto_importable_modules'] importable.known.update(user_importable) except Exception as ex: logger.info('ex = {!r}'.format(ex)) logger.info('ERROR user_importable = {!r}'.format(user_importable)) kw = {'importable': importable} if exists(fpath_or_text): kw['fpath'] = fpath_or_text else: kw['source'] = fpath_or_text lines = xinspect.autogen_imports(**kw) x = group_items(lines, [x.startswith('from ') for x in lines]) ordered_lines = [] ordered_lines += sorted(x.get(False, [])) ordered_lines += sorted(x.get(True, [])) import_block = '\n'.join(ordered_lines) return import_block
[docs] def is_url(text): """ heuristic check if str is url formatted """ return any([ text.startswith('http://'), text.startswith('https://'), text.startswith('www.'), '.org/' in text, '.com/' in text, ])
[docs] def extract_url_embeding(word): """ parse several common ways to embed url within a "word" """ # rst url embedding if word.startswith('<') and word.endswith('>`_'): word = word[1:-3] if word.startswith('(') and word.endswith(')'): word = word[1:-1] # markdown url embedding if word.startswith('[') and word.endswith(')'): import parse pres = parse.parse('[{tag}]({ref})', word) if pres: word = pres.named['ref'] return word
[docs] def find_and_open_path(path, mode='split', verbose=0, enable_python=True, enable_url=True, enable_cli=True): """ Fancy-Find. Does some magic to try and find the correct path. Currently supports: * well-formed absolute and relatiave paths * ill-formed relative paths when you are in a descendant directory * python modules that exist in the PYTHONPATH """ import os def try_open(path, step=''): # base = '/home/joncrall/code/VIAME/packages/kwiver/sprokit/src/bindings/python/sprokit/pipeline' # base = '/home' if path and exists(path): if verbose: print('EXISTS path = {!r}\n'.format(path)) open_path(path, mode=mode, verbose=verbose) return True else: print(f'Tried {step}, but failed: path={path}') def expand_module(path): # TODO: use ubelt util_import instead _debug = 0 if _debug: import sys print('sys.base_exec_prefix = {!r}'.format(sys.base_exec_prefix)) print('sys.base_prefix = {!r}'.format(sys.base_prefix)) print('sys.exec_prefix = {!r}'.format(sys.exec_prefix)) print('sys.executable = {!r}'.format(sys.executable)) print('sys.implementation = {!r}'.format(sys.implementation)) print('sys.prefix = {!r}'.format(sys.prefix)) print('sys.version = {!r}'.format(sys.version)) print('sys.path = {!r}'.format(sys.path)) import sys extra_path = CONFIG.get('vimtk_sys_path') sys_path = sys.path + [expandpath(p) for p in extra_path] print('expand path = {!r}'.format(path)) modparts = path.split('.') for i in reversed(range(1, len(modparts) + 1)): candidate = '.'.join(modparts[0:i]) print('candidate = {!r}'.format(candidate)) path = util.modname_to_modpath(candidate, sys_path=sys_path) if path is not None: break print('expanded modname-to-path = {!r}'.format(path)) return path def expand_module_prefix(path): # TODO: we could parse the AST to figure out if the prefix is an alias # for a known module. # Check if the path certainly looks like it could be a chain of python # attribute accessors. if re.match(r'^[\w\d_.]*$', path): extra_path = CONFIG.get('vimtk_sys_path') sys_path = sys.path + [expandpath(p) for p in extra_path] parts = path.split('.') for i in reversed(range(len(parts))): prefix = '.'.join(parts[:i]) path = util.modname_to_modpath(prefix, sys_path=sys_path) if path is not None: print('expanded prefix = {!r}'.format(path)) return path print('expanded prefix = {!r}'.format(None)) return None if enable_url: # https://github.com/Erotemic url = extract_url_embeding(path) if is_url(url): import webbrowser webbrowser.open(url) # browser = webbrowser.get('google-chrome') # browser.open(url) return path = expanduser(path) if try_open(path, 'after expand'): return if try_open(os.path.expandvars(path), 'after expandvars'): return if enable_cli: # Strip off the --argname= prefix match = re.match(r'--[\w_]*=', path) if match: path = path[match.end():] # path = 'sprokit/pipeline/pipeline.h' # base = os.getcwd() # base = '/home/joncrall/code/VIAME/packages/kwiver/sprokit/src/bindings/python/sprokit/pipeline' if path.startswith('<') and path.endswith('>'): path = path[1:-1] if path.startswith('`') and path.endswith('`'): path = path[1:-1] if path.endswith('>`_'): path = path[:-3] IGNORE_START_CHARS = tuple('<`') IGNORE_END_CHARS = tuple('>`') while path.startswith(IGNORE_START_CHARS): path = path[1:] while path.endswith(IGNORE_END_CHARS): path = path[:-1] if path.endswith(':'): path = path[:-1] if path.endswith('>`_'): path = path[:-3] if path.endswith('>`_.'): path = path[:-4] if path.endswith('>`_,'): path = path[:-4] path = os.path.expandvars(path) path = expanduser(path) # expand again in case a prefix was removed if try_open(path, 'after rst hacks'): return # Handle the case where the path is a bash environ assignment_pat = re.compile(r'^[^\d\W]\w*=') path = assignment_pat.sub('', path, count=1) path = expanduser(path) # expand again in case a prefix was removed if try_open(path, 'after varname= hacks'): return def ancestor_paths(start=None, limit={}): """ All paths above you """ limit = {expanduser(p) for p in limit}.union(set(limit)) if start is None: start = os.getcwd() path = start prev = None while path != prev and prev not in limit: yield path prev = path path = os.path.dirname(path) def search_candidate_paths(candidate_path_list, candidate_name_list=None, priority_paths=None, required_subpaths=[], verbose=None): """ searches for existing paths that meed a requirement Args: candidate_path_list (list): list of paths to check. If candidate_name_list is specified this is the dpath list instead candidate_name_list (list): specifies several names to check (default = None) priority_paths (None): specifies paths to check first. Ignore candidate_name_list (default = None) required_subpaths (list): specified required directory structure (default = []) verbose (bool): verbosity flag(default = True) Returns: str: return_path CommandLine: xdoctest -m utool.util_path --test-search_candidate_paths Example: >>> # DISABLE_DOCTEST >>> from utool.util_path import * # NOQA >>> import pathlib >>> candidate_path_list = [pathlib.Path('~/code/ubelt').expanduser()] >>> candidate_name_list = None >>> required_subpaths = [] >>> verbose = True >>> priority_paths = None >>> return_path = search_candidate_paths(candidate_path_list, >>> candidate_name_list, >>> priority_paths, required_subpaths, >>> verbose) >>> result = ('return_path = %s' % (str(return_path),)) >>> print(result) """ if verbose is None: verbose = 1 if verbose >= 1: print('[search_candidate_paths] Searching for candidate paths') if candidate_name_list is not None: candidate_path_list_ = [join(dpath, fname) for dpath, fname in it.product(candidate_path_list, candidate_name_list)] else: candidate_path_list_ = candidate_path_list if priority_paths is not None: candidate_path_list_ = priority_paths + candidate_path_list_ return_path = None for path in candidate_path_list_: if path is not None and exists(path): if verbose >= 2: print('[search_candidate_paths] Found candidate directory %r' % (path,)) print('[search_candidate_paths] ... checking for approprate structure') # tomcat directory exists. Make sure it also contains a webapps dir subpath_list = [join(path, subpath) for subpath in required_subpaths] if all(exists(path_) for path_ in subpath_list): return_path = path if verbose >= 2: print('[search_candidate_paths] Found acceptable path') return return_path break if verbose >= 1: print('[search_candidate_paths] Failed to find acceptable path') return return_path # Search downwards for relative paths candidates = [] if not os.path.isabs(path): limit = {'~', os.path.expanduser('~')} start = os.getcwd() candidates += list(ancestor_paths(start, limit=limit)) candidates += os.environ['PATH'].split(os.sep) result = search_candidate_paths(candidates, [path], verbose=verbose) if result is not None: path = result current_fpath = get_current_fpath() if os.path.islink(current_fpath): newbase = os.path.dirname(os.path.realpath(current_fpath)) resolved_path = os.path.join(newbase, path) if try_open(resolved_path): return if try_open(path): return else: print('enable_python = {!r}'.format(enable_python)) if enable_python: pypath = expand_module(path) print('pypath = {!r}'.format(pypath)) if try_open(pypath): return pypath = expand_module_prefix(path) print('pypath = {!r}'.format(pypath)) if try_open(pypath): return if re.match(r'--\w*=.*', path): # try and open if its a command line arg stripped_path = expanduser(re.sub(r'--\w*=', '', path)) if try_open(stripped_path): return #vim.command('echoerr "Could not find path={}"'.format(path)) print('Could not find path={!r}'.format(path))
[docs] def open_path(fpath, mode='e', nofoldenable=False, verbose=0): """ Execs new splits / tabs / etc Weird this wont work with directories (on my machine), see [vim_split_issue]_. Args: fpath : file path to open mode: how to open the new file (valid options: split, vsplit, tabe, e, new, ...) References: .. [vim_split_issue] https://superuser.com/questions/1243344/vim-wont-split-open-a-directory-from-python-but-it-works-interactively Ignore: ~/.bashrc ~/code """ import vim fpath = expanduser(fpath) if not exists(fpath): print("FPATH DOES NOT EXIST") # command = '{cmd} {fpath}'.format(cmd=cmd, fpath=fpath) if isdir(fpath): # Hack around directory problem if mode.startswith('e'): command = ':Explore! {fpath}'.format(fpath=fpath) elif mode.startswith('sp'): command = ':Hexplore! {fpath}'.format(fpath=fpath) elif mode.startswith('vs'): command = ':Vexplore! {fpath}'.format(fpath=fpath) else: raise NotImplementedError('implement fpath cmd for me') else: command = ":exec ':{mode} {fpath}'".format(mode=mode, fpath=fpath) if verbose: print('command = {!r}\n'.format(command)) try: vim.command(command) except Exception as ex: print('FAILED TO OPEN PATH') print('ex = {!r}'.format(ex)) raise pass if nofoldenable: vim.command(":set nofoldenable")
[docs] def find_pattern_above_row(pattern, line_list='current', row='current', maxIter=50): """ searches a few lines above the curror until it **matches** a pattern TODO: move to some class? Perhaps somethig like Finder? TODO: refactor """ import re if row == 'current': import vim row = vim.current.window.cursor[0] - 1 line_list = vim.current.buffer # Iterate until we match. # Janky way to find function / class name for ix in it.count(0): pos = row - ix if maxIter is not None and ix > maxIter: break if pos < 0: break searchline = line_list[pos] found = re.match(pattern, searchline) if found is not None: return searchline, pos, found return None
[docs] def get_first_nonempty_line_after_cursor(): import vim buf = vim.current.buffer (row, col) = vim.current.window.cursor for i in range(len(buf) - row): line = buf[row + i] if line: return line
[docs] def get_indentation(line_): """ returns the number of preceding spaces """ return len(line_) - len(line_.lstrip())
[docs] def get_minimum_indentation(text): r""" returns the number of preceding spaces Args: text (str): unicode text Returns: int: indentation Example: >>> text = ' foo\n bar' >>> result = get_minimum_indentation(text) >>> print(result) 3 """ lines = text.split('\n') indentations = [get_indentation(line_) for line_ in lines if len(line_.strip()) > 0] if len(indentations) == 0: return 0 return min(indentations)
[docs] def _linux_install(): """ Installs vimtk to the standard pathogen bundle directory """ import pkg_resources # NOQA import vimtk # NOQA
# TODO: finishme # vim_data = pkg_resources.resource_string(vimtk.__name__, "vim") CONFIG = Config() if __name__ == '__main__': r""" CommandLine: export PYTHONPATH=$PYTHONPATH:/home/joncrall/code/vimtk/autoload python ~/code/vimtk/autoload/vimtk.py """ import xdoctest xdoctest.doctest_module(__file__)