" Bicycle Repair Man integration for Vim " Version 0.3 " Copyright (c) 2003 Marius Gedminas " " Needs Vim 6.x with python interpreter (Python 2.2 or newer) " Installation instructions: just drop it into $HOME/.vim/plugin and you " should see the Bicycle Repair Menu appear in GVim (or you can check if " any of the BikeXxx commands are defined in console mode Vim). If bike.vim " fails to load and you want to see the reason, try " let g:bike_exceptions = 1 " source ~/.vim/plugin/bike.vim " " Configuration options you can add to your .vimrc: " let g:bike_exceptions = 1 " show tracebacks on exceptions " let g:bike_progress = 1 " show import progress " " Commands defined: " " BikeShowScope " Show current scope. Sample of usage: " autocmd CursorHold *.py BikeShowScope " " BikeShowType " Show selected expression type. The range is ignored, '<,'> is always " used. Use this in visual block mode. " " BikeFindRefs " Finds all references of the function/method/class defined in current " line. " " BikeFindDef " Finds the definition of the function/method/class under cursor. " " BikeRename " Renames the function/method/class defined in current line. Updates " all references in all files known (i.e. imported) to Bicycle Repair " Man. " " BikeExtract " Extracts a part of the function/method into a new function/method. " The range is ignored, '<,'> is always used. Use this in visual mode. " " BikeUndo " Globally undoes the previous refactoring. " " Issues: " - Saves all modified buffers to disk without asking the user -- not nice " - Does not reimport files that were modified outside of Vim " - Uses mktemp() -- I think that produces deprecation warnings in Python 2.3 " - BikeShowType, BikeExtract ignores the specified range, that's confusing. " At least BikeExtract ought to work... " - BikeShowType/BikeExtract or their GUI counterparts work when not in " visual mode by using the previous values of '<,'>. Might be confusing. " - Would be nice if :BikeImport asked to enter package " - Would be nice if :BikeRename myNewName worked " - The code is not very robust (grep for XXX) " " Default settings for global configuration variables {{{1 " " Set to 1 to see full tracebacks if !exists("g:bike_exceptions") let g:bike_exceptions = 0 endif " Set to 1 to see import progress if !exists("g:bike_progress") let g:bike_progress = 0 endif " " Initialization {{{1 " " First check that Vim is sufficiently recent, that we have python interpreted " support, and that Bicycle Repair Man is available. " " If something is wrong, fail silently, as error messages every time vim " starts are very annoying. But if the user wants to see why bike.vim failed " to load, she can let g:bike_exceptions = 1 if version < 600 if g:bike_exceptions echo 'Bicycle Repair Man needs Vim 6.0' endif finish endif if !has("python") if g:bike_exceptions echo 'Bicycle Repair Man needs Vim with Python interpreter support (2.2 or newer)' endif finish endif let s:has_bike=1 python << END import vim import sys try: if sys.version_info < (2, 2): raise ImportError, 'Bicycle Repair Man needs Python 2.2 or newer' import bike bikectx = bike.init() bikectx.isLoaded # make sure bike package is recent enough except ImportError: vim.command("let s:has_bike=0") if vim.eval('g:bike_exceptions') not in (None, '', '0'): raise END if !s:has_bike finish endif " Use sane cpoptions let s:cpo_save = &cpo set cpo&vim " " Menu {{{1 " silent! aunmenu Bicycle\ Repair\ Man " Shortcuts available: ab-----h-jk--nopq----vw--z amenu Bicycle\ &Repair\ Man.-SEP1- \ : amenu Bicycle\ &Repair\ Man.&Find\ References \ :call BikeFindRefs() amenu Bicycle\ &Repair\ Man.Find\ &Definition \ :call BikeFindDef() amenu Bicycle\ &Repair\ Man.Resu<s.&List:cl \ :cl amenu Bicycle\ &Repair\ Man.Resu<s.&Current:cc \ :cc amenu Bicycle\ &Repair\ Man.Resu<s.&Next:cn \ :cn amenu Bicycle\ &Repair\ Man.Resu<s.&Previous:cp \ :cp amenu Bicycle\ &Repair\ Man.Resu<s.&First:cfirst \ :cfirst amenu Bicycle\ &Repair\ Man.Resu<s.Las&t:clast \ :clast amenu Bicycle\ &Repair\ Man.Resu<s.&Older\ List:colder \ :colder amenu Bicycle\ &Repair\ Man.Resu<s.N&ewer\ List:cnewer \ :cnewer amenu Bicycle\ &Repair\ Man.Resu<s.&Window.&Update:cw \ :cw amenu Bicycle\ &Repair\ Man.Resu<s.&Window.&Open:copen \ :copen amenu Bicycle\ &Repair\ Man.Resu<s.&Window.&Close:cclose \ :cclose amenu Bicycle\ &Repair\ Man.-SEP2- \ : amenu Bicycle\ &Repair\ Man.&Rename \ :call BikeRename() amenu Bicycle\ &Repair\ Man.E&xtract\ Method \ :call BikeExtract('method') amenu Bicycle\ &Repair\ Man.&Extract\ Function \ :call BikeExtract('function') amenu Bicycle\ &Repair\ Man.&Undo \ :call BikeUndo() amenu Bicycle\ &Repair\ Man.-SEP3- \ : amenu Bicycle\ &Repair\ Man.Settin&gs.Import\ &Progress.&Enable \ :let g:bike_progress = 1 amenu Bicycle\ &Repair\ Man.Settin&gs.Import\ &Progress.&Disable \ :let g:bike_progress = 0 amenu Bicycle\ &Repair\ Man.Settin&gs.Full\ &Exceptions.&Enable \ :let g:bike_exceptions = 1 amenu Bicycle\ &Repair\ Man.Settin&gs.Full\ &Exceptions.&Disable \ :let g:bike_exceptions = 0 " Note: The three rename commands are basically identical. The two extract " commands are also identical behind the scenes. " " Commands {{{1 " command! BikeShowScope call BikeShowScope() command! -range BikeShowType call BikeShowType() command! BikeFindRefs call BikeFindRefs() command! BikeFindDef call BikeFindDef() command! BikeRename call BikeRename() command! -range BikeExtract call BikeExtract('function') command! BikeUndo call BikeUndo() " " Implementation {{{1 " " Query functions {{{2 function! s:BikeShowScope() " Shows the scope under cursor python << END fn = vim.current.buffer.name row, col = vim.current.window.cursor try: print bikectx.getFullyQualifiedNameOfScope(fn, row) except: show_exc() END endf function! s:BikeShowType() " Shows the inferred type of the selected expression if col("'<") == 0 " mark not set echo "Select a region first!" return endif if line("'<") != line("'>") " multiline selection echo "Multi-line regions not supported" " XXX deficiency of bikefacade interface, expressions can easily span " several lines in Python return endif python << END fn = vim.current.buffer.name row1, col1 = vim.current.buffer.mark('<') row2, col2 = vim.current.buffer.mark('>') try: print bikectx.getTypeOfExpression(fn, row1, col1, col2) except: show_exc() END endf function! s:BikeFindRefs() " Find all references to the item defined on current line wall python << END fn = vim.current.buffer.name row, col = vim.current.window.cursor try: refs = bikectx.findReferencesByCoordinates(fn, row, col) quickfixdefs(refs) except: show_exc() END endf function! s:BikeFindDef() " Find all definitions of the item under cursor. Ideally there should be only " one. wall python << END fn = vim.current.buffer.name row, col = vim.current.window.cursor try: defs = bikectx.findDefinitionByCoordinates(fn, row, col) quickfixdefs(defs) except: show_exc() END endf " Refactoring commands {{{2 function! s:BikeRename() " Rename a function/method/class. let newname = inputdialog('Rename to: ') wall python << END fn = vim.current.buffer.name row, col = vim.current.window.cursor newname = vim.eval("newname") if newname: try: bikectx.setRenameMethodPromptCallback(renameMethodPromptCallback) bikectx.renameByCoordinates(fn, row, col, newname) except: show_exc() saveChanges() else: print "Aborted" END endf function! s:BikeExtract(what) " Extract a piece of code into a separate function/method. The argument " a:what can be 'method' or 'function'. if col("'<") == 0 " mark not set echo "Select a region first!" return endif let newname = inputdialog('New function name: ') wall python << END fn = vim.current.buffer.name row1, col1 = vim.current.buffer.mark('<') row2, col2 = vim.current.buffer.mark('>') newname = vim.eval("newname") if newname: try: bikectx.extractMethod(fn, row1, col1, row2, col2, newname) except: show_exc() saveChanges() else: print "Aborted" END endf function! s:BikeUndo() " Undoes the last refactoring wall python << END try: bikectx.undo() saveChanges() except bike.UndoStackEmptyException, e: print "Nothing to undo" END endf " " Helper functions {{{1 " " Python helpers python << END import tempfile import os import linecache modified = {} # a dictionary whose keys are the names of files modifed # in Vim since the last import def show_exc(): """Print exception according to bike settings.""" if vim.eval('g:bike_exceptions') not in (None, '', '0'): import traceback traceback.print_exc() else: type, value = sys.exc_info()[:2] if value is not None: print "%s: %s" % (type, value) else: print type def writedefs(defs, filename): """Write a list of file locations to a file.""" ef = None curdir = os.getcwd() if not curdir.endswith(os.sep): curdir = curdir + os.sep for d in defs: if not ef: ef = open(filename, "w") fn = d.filename if fn.startswith(curdir): fn = fn[len(curdir):] line = linecache.getline(fn, d.lineno).strip() res ="%s:%d: %3d%%: %s" % (fn, d.lineno, d.confidence, line) print res print >> ef, res if ef: ef.close() return ef is not None def quickfixdefs(defs): """Import a list of file locations into vim error list.""" fn = tempfile.mktemp() # XXX unsafe if writedefs(defs, fn): vim.command('let old_errorfile = &errorfile') vim.command('let old_errorformat = &errorformat') vim.command(r'set errorformat=\%f:\%l:\ \%m') vim.command("cfile %s" % fn) vim.command('let &errorformat = old_errorformat') vim.command('let &errorfile = old_errorfile') os.unlink(fn) else: print "Not found" def renameMethodPromptCallback(filename, line, col1, col2): """Verify that the call in a given file position should be renamed.""" vim.command('e +%d %s' % (line, filename)) vim.command('normal %d|' % col1) vim.command('match Search /\%%%dl\%%>%dc\%%<%dc' % (line, col1, col2+1)) vim.command('redraw') ans = vim.eval('confirm("Cannot deduce instance type. Rename this call?",' ' "&Yes\n&No", 1, "Question")') vim.command('match none') if ans == '1': return 1 else: return 0 def saveChanges(): """Save refactoring changes to the file system and reload the modified files in Vim.""" # bikectx.save() returns a list of modified file names. We should make # sure that all those files are reloaded iff they were open in vim. files = map(os.path.abspath, bikectx.save()) opened_files = {} for b in vim.buffers: if b.name is not None: opened_files[os.path.abspath(b.name)] = b.name for f in files: try: # XXX might fail when file name contains funny characters vim.command("sp %s | q" % opened_files[f]) except KeyError: pass # Just in case: vim.command("checktime") vim.command("e") END " Make sure modified files are reimported into BRM autocmd BufWrite * python modified[os.path.abspath(vim.current.buffer.name)] = 1 " " Cleanup {{{1 " " Restore cpoptions let &cpo = s:cpo_save unlet s:cpo_save