vim/sadness/ropevim/src/ropevim/ropevim.py @ c925b9fad53c

vim: can I start drinking yet?
author Steve Losh <steve@stevelosh.com>
date Tue, 10 May 2011 11:27:43 -0400
parents 48cacfdc2ca6
children (none)
"""ropevim, a vim mode for using rope refactoring library"""
import os
import tempfile
import re

import ropemode.decorators
import ropemode.environment
import ropemode.interface

import vim


class VimUtils(ropemode.environment.Environment):

    def ask(self, prompt, default=None, starting=None):
        if starting is None:
            starting = ''
        if default is not None:
            prompt = prompt + ('[%s] ' % default)
        result = call('input("%s", "%s")' % (prompt, starting))
        if default is not None and result == '':
            return default
        return result

    def ask_values(self, prompt, values, default=None,
                   starting=None, show_values=None):
        if show_values or (show_values is None and len(values) < 14):
            self._print_values(values)
        if default is not None:
            prompt = prompt + ('[%s] ' % default)
        starting = starting or ''
        _completer.values = values
        answer = call('input("%s", "%s", "customlist,RopeValueCompleter")' %
                      (prompt, starting))
        if answer is None:
            if 'cancel' in values:
                return 'cancel'
            return
        if default is not None and not answer:
            return default
        if answer.isdigit() and 0 <= int(answer) < len(values):
            return values[int(answer)]
        return answer

    def _print_values(self, values):
        numbered = []
        for index, value in enumerate(values):
            numbered.append('%s. %s' % (index, str(value)))
        echo('\n'.join(numbered) + '\n')

    def ask_directory(self, prompt, default=None, starting=None):
        return call('input("%s", ".", "dir")' % prompt)

    def ask_completion(self, prompt, values, starting=None):
        if self.get('vim_completion') and 'i' in call('mode()'):
            if not self.get('extended_complete', False):
                proposals = u','.join(u"'%s'" % self._completion_text(proposal)
                                      for proposal in values)
            else:
                proposals = u','.join(self._extended_completion(proposal)
                                      for proposal in values)

            col = int(call('col(".")'))
            if starting:
                col -= len(starting)
            command = u'call complete(%s, [%s])' % (col, proposals)
            vim.command(command.encode(self._get_encoding()))
            return None
        return self.ask_values(prompt, values, starting=starting,
                               show_values=False)

    def message(self, message):
        echo(message)

    def yes_or_no(self, prompt):
        return self.ask_values(prompt, ['yes', 'no']) == 'yes'

    def y_or_n(self, prompt):
        return self.yes_or_no(prompt)

    def get(self, name, default=None):
        vimname = 'g:ropevim_%s' % name
        if str(vim.eval('exists("%s")' % vimname)) == '0':
            return default
        result = vim.eval(vimname)
        if isinstance(result, str) and result.isdigit():
            return int(result)
        return result

    def get_offset(self):
        result = self._position_to_offset(*self.cursor)
        return result

    def _get_encoding(self):
        return vim.eval('&encoding')
    def _encode_line(self, line):
        return line.encode(self._get_encoding())
    def _decode_line(self, line):
        return line.decode(self._get_encoding())

    def _position_to_offset(self, lineno, colno):
        result = min(colno, len(self.buffer[lineno -1]) + 1)
        for line in self.buffer[:lineno-1]:
            line = self._decode_line(line)
            result += len(line) + 1
        return result

    def get_text(self):
        return self._decode_line('\n'.join(self.buffer)) + u'\n'

    def get_region(self):
        start = self._position_to_offset(*self.buffer.mark('<'))
        end = self._position_to_offset(*self.buffer.mark('>'))
        return start, end

    @property
    def buffer(self):
        return vim.current.buffer

    def _get_cursor(self):
        lineno, col = vim.current.window.cursor
        line = self._decode_line(vim.current.line[:col])
        col = len(line)
        return (lineno, col)

    def _set_cursor(self, cursor):
        lineno, col = cursor
        line = self._decode_line(vim.current.line)
        line = self._encode_line(line[:col])
        col = len(line)
        vim.current.window.cursor = (lineno, col)

    cursor = property(_get_cursor, _set_cursor)

    def filename(self):
        return self.buffer.name

    def is_modified(self):
        return vim.eval('&modified')

    def goto_line(self, lineno):
        self.cursor = (lineno, 0)

    def insert_line(self, line, lineno):
        self.buffer[lineno - 1:lineno - 1] = [line]

    def insert(self, text):
        lineno, colno = self.cursor
        line = self.buffer[lineno - 1]
        self.buffer[lineno - 1] = line[:colno] + text + line[colno:]
        self.cursor = (lineno, colno + len(text))

    def delete(self, start, end):
        lineno1, colno1 = self._offset_to_position(start - 1)
        lineno2, colno2 = self._offset_to_position(end - 1)
        lineno, colno = self.cursor
        if lineno1 == lineno2:
            line = self.buffer[lineno1 - 1]
            self.buffer[lineno1 - 1] = line[:colno1] + line[colno2:]
            if lineno == lineno1 and colno >= colno1:
                diff = colno2 - colno1
                self.cursor = (lineno, max(0, colno - diff))

    def _offset_to_position(self, offset):
        text = self.get_text()
        lineno = text.count('\n', 0, offset) + 1
        try:
            colno = offset - text.rindex('\n', 0, offset) - 1
        except ValueError:
            colno = offset
        return lineno, colno

    def filenames(self):
        result = []
        for buffer in vim.buffers:
            if buffer.name:
                result.append(buffer.name)
        return result

    def save_files(self, filenames):
        vim.command('wall')

    def reload_files(self, filenames, moves={}):
        initial = self.filename()
        for filename in filenames:
            self.find_file(moves.get(filename, filename), force=True)
        if initial:
            self.find_file(initial)

    def find_file(self, filename, readonly=False, other=False, force=False):
        if filename != self.filename() or force:
            if other:
                vim.command('new')
            vim.command('e %s' % filename)
            if readonly:
                vim.command('set nomodifiable')

    def create_progress(self, name):
        return VimProgress(name)

    def current_word(self):
        return vim.eval('expand("<cword>")')

    def push_mark(self):
        vim.command('mark `')

    def prefix_value(self, prefix):
        return prefix

    def show_occurrences(self, locations):
        self._quickfixdefs(locations)

    def _quickfixdefs(self, locations):
        filename = os.path.join(tempfile.gettempdir(), tempfile.mktemp())
        try:
            self._writedefs(locations, filename)
            vim.command('let old_errorfile = &errorfile')
            vim.command('let old_errorformat = &errorformat')
            vim.command('set errorformat=%f:%l:\ %m')
            vim.command('cfile ' + filename)
            vim.command('let &errorformat = old_errorformat')
            vim.command('let &errorfile = old_errorfile')
        finally:
            os.remove(filename)

    def _writedefs(self, locations, filename):
        tofile = open(filename, 'w')
        try:
            for location in locations:
                err = '%s:%d: - %s\n' % (location.filename,
                                         location.lineno, location.note)
                echo(err)
                tofile.write(err)
        finally:
            tofile.close()

    def show_doc(self, docs, altview=False):
        if docs:
            echo(docs)

    def preview_changes(self, diffs):
        echo(diffs)
        return self.y_or_n('Do the changes? ')

    def local_command(self, name, callback, key=None, prefix=False):
        self._add_command(name, callback, key, prefix,
                          prekey=self.get('local_prefix'))

    def global_command(self, name, callback, key=None, prefix=False):
        self._add_command(name, callback, key, prefix,
                          prekey=self.get('global_prefix'))

    def add_hook(self, name, callback, hook):
        mapping = {'before_save': 'FileWritePre,BufWritePre',
                   'after_save': 'FileWritePost,BufWritePost',
                   'exit': 'VimLeave'}
        self._add_function(name, callback)
        vim.command('autocmd %s *.py call %s()' %
                    (mapping[hook], _vim_name(name)))

    def _add_command(self, name, callback, key, prefix, prekey):
        self._add_function(name, callback, prefix)
        vim.command('command! -range %s call %s()' %
                    (_vim_name(name), _vim_name(name)))
        if key is not None:
            key = prekey + key.replace(' ', '')
            vim.command('map %s :call %s()<cr>' % (key, _vim_name(name)))

    def _add_function(self, name, callback, prefix=False):
        globals()[name] = callback
        arg = 'None' if prefix else ''
        vim.command('function! %s()\n' % _vim_name(name) +
                    'python ropevim.%s(%s)\n' % (name, arg) +
                    'endfunction\n')

    def _completion_data(self, proposal):
        return proposal

    _docstring_re = re.compile('^[\s\t\n]*([^\n]*)')
    def _extended_completion(self, proposal):
        # we are using extended complete and return dicts instead of strings.
        # `ci` means "completion item". see `:help complete-items`
        ci = {'word': proposal.name}

        scope = proposal.scope[0].upper()
        type_ = proposal.type
        info = None

        if proposal.scope == 'parameter_keyword':
            scope = ' '
            type_ = 'param'
            if not hasattr(proposal, 'get_default'):
                # old version of rope
                pass
            else:
                default = proposal.get_default()
                if default is None:
                    info = '*'
                else:
                    info = '= %s' % default

        elif proposal.scope == 'keyword':
            scope = ' '
            type_ = 'keywd'

        elif proposal.scope == 'attribute':
            scope = 'M'
            if proposal.type == 'function':
                type_ = 'meth'
            elif proposal.type == 'instance':
                type_ = 'prop'

        elif proposal.type == 'function':
            type_ = 'func'

        elif proposal.type == 'instance':
            type_ = 'inst'

        elif proposal.type == 'module':
            type_ = 'mod'

        if info is None:
            obj_doc = proposal.get_doc()
            if obj_doc:
                info = self._docstring_re.match(obj_doc).group(1)
            else:
                info = ''

        if type_ is None:
            type_ = ' '
        else:
            type_ = type_.ljust(5)[:5]
        ci['menu'] = ' '.join((scope, type_, info))
        ret =  u'{%s}' % \
               u','.join(u'"%s":"%s"' % \
                         (key, value.replace('"', '\\"')) \
                         for (key, value) in ci.iteritems())
        return ret


def _vim_name(name):
    tokens = name.split('_')
    newtokens = ['Rope'] + [token.title() for token in tokens]
    return ''.join(newtokens)


class VimProgress(object):

    def __init__(self, name):
        self.name = name
        self.last = 0
        echo('%s ... ' % self.name)

    def update(self, percent):
        try:
            vim.eval('getchar(0)')
        except vim.error:
            raise KeyboardInterrupt('Task %s was interrupted!' % self.name)
        if percent > self.last + 4:
            echo('%s ... %s%%%%' % (self.name, percent))
            self.last = percent

    def done(self):
        echo('%s ... done' % self.name)


def echo(message):
    if isinstance(message, unicode):
        message = message.encode(vim.eval('&encoding'))
    print message

def call(command):
    return vim.eval(command)


class _ValueCompleter(object):

    def __init__(self):
        self.values = []
        vim.command('python import vim')
        vim.command('function! RopeValueCompleter(A, L, P)\n'
                    'python args = [vim.eval("a:" + p) for p in "ALP"]\n'
                    'python ropevim._completer(*args)\n'
                    'return s:completions\n'
                    'endfunction\n')

    def __call__(self, arg_lead, cmd_line, cursor_pos):
        result = [proposal.name for proposal in self.values \
                  if proposal.name.startswith(arg_lead)]
        vim.command('let s:completions = %s' % result)


variables = {'ropevim_enable_autoimport': 1,
             'ropevim_autoimport_underlineds': 0,
             'ropevim_codeassist_maxfixes' : 1,
             'ropevim_enable_shortcuts' : 1,
             'ropevim_autoimport_modules': '[]',
             'ropevim_confirm_saving': 0,
             'ropevim_local_prefix': '"<C-c>r"',
             'ropevim_global_prefix': '"<C-x>p"',
             'ropevim_vim_completion': 0,
             'ropevim_guess_project': 0}

shortcuts = {'code_assist': '<M-/>',
             'lucky_assist': '<M-?>',
             'goto_definition': '<C-c>g',
             'show_doc': '<C-c>d',
             'find_occurrences': '<C-c>f'}

insert_shortcuts = {'code_assist': '<M-/>',
                    'lucky_assist': '<M-?>'}

def _init_variables():
    for variable, default in variables.items():
        vim.command('if !exists("g:%s")\n' % variable +
                    '  let g:%s = %s\n' % (variable, default))

def _enable_shortcuts(env):
    if env.get('enable_shortcuts'):
        for command, shortcut in shortcuts.items():
            vim.command('map %s :call %s()<cr>' %
                        (shortcut, _vim_name(command)))
        for command, shortcut in insert_shortcuts.items():
            command_name = _vim_name(command) + 'InsertMode'
            vim.command('func! %s()\n' % command_name +
                        'call %s()\n' % _vim_name(command) +
                        'return ""\n'
                        'endfunc')
            vim.command('imap %s <C-R>=%s()<cr>' % (shortcut, command_name))

def _add_menu(env):
    project = ['open_project', 'close_project', 'find_file', 'undo', 'redo']
    refactor = ['rename', 'extract_variable', 'extract_method', 'inline',
                'move', 'restructure', 'use_function', 'introduce_factory',
                'change_signature', 'rename_current_module',
                'move_current_module', 'module_to_package']
    assists = ['code_assist', 'goto_definition', 'show_doc', 'find_occurrences',
               'lucky_assist', 'jump_to_global', 'show_calltip']
    vim.command('silent! aunmenu Ropevim')
    for index, items in enumerate([project, assists, refactor]):
        if index != 0:
            vim.command('amenu <silent> &Ropevim.-SEP%s- :' % index)
        for name in items:
            item = '\ '.join(token.title() for token in name.split('_'))
            for command in ['amenu', 'vmenu']:
                vim.command('%s <silent> &Ropevim.%s :call %s()<cr>' %
                            (command, item, _vim_name(name)))


ropemode.decorators.logger.message = echo
ropemode.decorators.logger.only_short = True
_completer = _ValueCompleter()

_init_variables()
_env = VimUtils()
_interface = ropemode.interface.RopeMode(env=_env)
_interface.init()
_enable_shortcuts(_env)
_add_menu(_env)