|
| 1 | +" File: pythonhelper.vim |
| 2 | +" Author: Michal Vitecek <fuf-at-mageo-dot-cz> |
| 3 | +" Version: 0.7 |
| 4 | +" Last Modified: Oct 2, 2002 |
| 5 | +" |
| 6 | +" Overview |
| 7 | +" -------- |
| 8 | +" Vim script to help moving around in larger Python source files. It displays |
| 9 | +" current class, method or function the cursor is placed in in the status |
| 10 | +" line for every python file. It's more clever than Yegappan Lakshmanan's |
| 11 | +" taglist.vim because it takes into account indetation and comments to |
| 12 | +" determine what tag the cursor is placed in. |
| 13 | +" |
| 14 | +" Requirements |
| 15 | +" ------------ |
| 16 | +" This script needs VIM compiled with Python interpreter and relies on |
| 17 | +" exuberant ctags utility to generate the tag listing. You can determine |
| 18 | +" whether your VIM has Python support by issuing command :ver and looking for |
| 19 | +" +python in the list of features. |
| 20 | +" |
| 21 | +" The exuberant ctags can be downloaded from http://ctags.sourceforge.net/ and |
| 22 | +" should be reasonably new version (tested with 5.3). |
| 23 | +" |
| 24 | +" Note: The script doesn't display current tag on the status line only in |
| 25 | +" NORMAL mode. This is because CursorHold event is fired up only in this mode. |
| 26 | +" However if you badly need to know what tag you are on even in INSERT or |
| 27 | +" VISUAL mode, contact me on the above specified email address and I'll send |
| 28 | +" you patch that enables it. |
| 29 | +" |
| 30 | +" Installation |
| 31 | +" ------------ |
| 32 | +" 1. Make sure your Vim has python feature on (+python). If not, you will need |
| 33 | +" to recompile it with --with-pythoninterp option to the configure script |
| 34 | +" 2. Copy script pythonhelper.vim to the $HOME/.vim/plugin directory |
| 35 | +" 3. Edit the script and modify the location of your exuberant tags utility |
| 36 | +" (variable CTAGS_PROGRAM). |
| 37 | +" 4. Run Vim and open any python file. |
| 38 | +" |
| 39 | +python << EOS |
| 40 | + |
| 41 | +# import of required modules {{{ |
| 42 | +import vim |
| 43 | +import os |
| 44 | +import popen2 |
| 45 | +import time |
| 46 | +import sys |
| 47 | +# }}} |
| 48 | + |
| 49 | + |
| 50 | +# CTAGS program and parameters {{{ |
| 51 | +CTAGS_PROGRAM = "/usr/local/bin/ctags" |
| 52 | +CTAGS_PARAMETERS = "--language-force=python --format=2 --sort=0 --fields=+nK -L - -f - " |
| 53 | +# }}} |
| 54 | + |
| 55 | +# global dictionaries of tags and their line numbers, keys are buffer numbers {{{ |
| 56 | +TAGS = {} |
| 57 | +TAGLINENUMBERS = {} |
| 58 | +BUFFERTICKS = {} |
| 59 | +# }}} |
| 60 | + |
| 61 | + |
| 62 | +def getNearestLineIndex(row, tagLineNumbers): |
| 63 | + # DOC {{{ |
| 64 | + """Returns index of line in tagLineNumbers list that is nearest to the |
| 65 | + current cursor row. |
| 66 | + |
| 67 | + Parameters |
| 68 | + |
| 69 | + row -- current cursor row |
| 70 | + |
| 71 | + tagLineNumbers -- list of tags' line numbers (ie. their position) |
| 72 | + """ |
| 73 | + # }}} |
| 74 | + |
| 75 | + # CODE {{{ |
| 76 | + nearestLineNumber = -1 |
| 77 | + nearestLineIndex = -1 |
| 78 | + i = 0 |
| 79 | + for lineNumber in tagLineNumbers: |
| 80 | + # if the current line is nearer the current cursor position, take it {{{ |
| 81 | + if (nearestLineNumber < lineNumber <= row): |
| 82 | + nearestLineNumber = lineNumber |
| 83 | + nearestLineIndex = i |
| 84 | + # }}} |
| 85 | + # if we've got past the current cursor position, let's end the search {{{ |
| 86 | + if (lineNumber >= row): |
| 87 | + break |
| 88 | + # }}} |
| 89 | + i += 1 |
| 90 | + return nearestLineIndex |
| 91 | + # }}} |
| 92 | + |
| 93 | + |
| 94 | +def getTags(bufferNumber, changedTick): |
| 95 | + # DOC {{{ |
| 96 | + """Reads the tags for the specified buffer number. It does so by executing |
| 97 | + the CTAGS program and parsing its output. Returns tuple |
| 98 | + (taglinenumber[buffer], tags[buffer]). |
| 99 | + |
| 100 | + Parameters |
| 101 | + |
| 102 | + bufferNumber -- number of the current buffer |
| 103 | + |
| 104 | + changedTick -- ever increasing number used to tell if the buffer has |
| 105 | + been modified since the last time |
| 106 | + """ |
| 107 | + # }}} |
| 108 | + |
| 109 | + # CODE {{{ |
| 110 | + global CTAGS_PROGRAM, CTAGS_PARAMETERS |
| 111 | + global TAGLINENUMBERS, TAGS, BUFFERTICKS |
| 112 | + |
| 113 | + |
| 114 | + # return immediately if there's no need to update the tags {{{ |
| 115 | + if ((BUFFERTICKS.has_key(bufferNumber)) and (BUFFERTICKS[bufferNumber] == changedTick)): |
| 116 | + return (TAGLINENUMBERS[bufferNumber], TAGS[bufferNumber],) |
| 117 | + # }}} |
| 118 | + |
| 119 | + # read the tags and fill the global variables {{{ |
| 120 | + currentBuffer = vim.current.buffer |
| 121 | + currentWindow = vim.current.window |
| 122 | + row, col = currentWindow.cursor |
| 123 | + |
| 124 | + # create a temporary file with the current content of the buffer {{{ |
| 125 | + fileName = "/tmp/.%s.%u.ph" % (os.path.basename(currentBuffer.name), os.getpid(),) |
| 126 | + f = open(fileName, "w") |
| 127 | + |
| 128 | + for line in currentBuffer: |
| 129 | + f.write(line) |
| 130 | + f.write('\n') |
| 131 | + f.close() |
| 132 | + # }}} |
| 133 | + |
| 134 | + # run ctags on it {{{ |
| 135 | + try: |
| 136 | + ctagsOutPut, ctagsInPut = popen2.popen4("%s %s" % (CTAGS_PROGRAM, CTAGS_PARAMETERS,)) |
| 137 | + ctagsInPut.write(fileName + "\n") |
| 138 | + ctagsInPut.close() |
| 139 | + except: |
| 140 | + os.unlink(fileName) |
| 141 | + return |
| 142 | + # }}} |
| 143 | + |
| 144 | + # parse the ctags' output {{{ |
| 145 | + tagLineNumbers = [] |
| 146 | + tags = {} |
| 147 | + while 1: |
| 148 | + line = ctagsOutPut.readline() |
| 149 | + # if empty line has been read, it's the end of the file {{{ |
| 150 | + if (line == ''): |
| 151 | + break |
| 152 | + # }}} |
| 153 | + # if the line starts with !, then it's a comment line {{{ |
| 154 | + if (line[0] == '!'): |
| 155 | + continue |
| 156 | + # }}} |
| 157 | + |
| 158 | + # split the line into parts and parse the data {{{ |
| 159 | + # the format is: [0]tagName [1]fileName [2]tagLine [3]tagType [4]tagLineNumber [[5]tagOwner] |
| 160 | + tagData = line.split('\t') |
| 161 | + name = tagData[0] |
| 162 | + # get the tag's indentation {{{ |
| 163 | + start = 2 |
| 164 | + j = 2 |
| 165 | + while ((j < len(tagData[2])) and (tagData[2][j].isspace())): |
| 166 | + if (tagData[2][j] == '\t'): |
| 167 | + start += 8 |
| 168 | + else: |
| 169 | + start += 1 |
| 170 | + j += 1 |
| 171 | + # }}} |
| 172 | + type = tagData[3] |
| 173 | + line = int(tagData[4][5:]) |
| 174 | + if (len(tagData) == 6): |
| 175 | + owner = tagData[5].strip() |
| 176 | + else: |
| 177 | + owner = None |
| 178 | + # }}} |
| 179 | + tagLineNumbers.append(line) |
| 180 | + tags[line] = (name, type, owner, start) |
| 181 | + ctagsOutPut.close() |
| 182 | + # }}} |
| 183 | + |
| 184 | + # clean up the now unnecessary stuff {{{ |
| 185 | + os.unlink(fileName) |
| 186 | + # }}} |
| 187 | + |
| 188 | + # update the global variables {{{ |
| 189 | + TAGS[bufferNumber] = tags |
| 190 | + TAGLINENUMBERS[bufferNumber] = tagLineNumbers |
| 191 | + BUFFERTICKS[bufferNumber] = changedTick |
| 192 | + # }}} |
| 193 | + # }}} |
| 194 | + |
| 195 | + return (TAGLINENUMBERS[bufferNumber], TAGS[bufferNumber],) |
| 196 | + # }}} |
| 197 | + |
| 198 | + |
| 199 | +def findTag(bufferNumber, changedTick): |
| 200 | + # DOC {{{ |
| 201 | + """Tries to find the best tag for the current cursor position. |
| 202 | + |
| 203 | + Parameters |
| 204 | + |
| 205 | + bufferNumber -- number of the current buffer |
| 206 | + |
| 207 | + changedTick -- ever increasing number used to tell if the buffer has |
| 208 | + been modified since the last time |
| 209 | + """ |
| 210 | + # }}} |
| 211 | + |
| 212 | + # CODE {{{ |
| 213 | + try: |
| 214 | + # get the tags data for the current buffer |
| 215 | + tagLineNumbers, tags = getTags(bufferNumber, changedTick) |
| 216 | + |
| 217 | + # link to vim internal data {{{ |
| 218 | + currentBuffer = vim.current.buffer |
| 219 | + currentWindow = vim.current.window |
| 220 | + row, col = currentWindow.cursor |
| 221 | + # }}} |
| 222 | + |
| 223 | + # get the index of the nearest line |
| 224 | + nearestLineIndex = getNearestLineIndex(row, tagLineNumbers) |
| 225 | + # if any line was found, try to find if the tag is appropriate {{{ |
| 226 | + # (ie. the cursor can be below the last tag but on a code that has nothing |
| 227 | + # to do with the tag, because it's indented differently, in such case no |
| 228 | + # appropriate tag has been found.) |
| 229 | + if (nearestLineIndex > -1): |
| 230 | + nearestLineNumber = tagLineNumbers[nearestLineIndex] |
| 231 | + # walk through all the lines in range (nearestTagLine, cursorRow) {{{ |
| 232 | + for i in xrange(nearestLineNumber + 1, row): |
| 233 | + line = currentBuffer[i] |
| 234 | + # count the indentation of the line, if it's lower that the tag's, the found tag is wrong {{{ |
| 235 | + if (len(line)): |
| 236 | + # compute the indentation of the line {{{ |
| 237 | + lineStart = 0 |
| 238 | + j = 0 |
| 239 | + while ((j < len(line)) and (line[j].isspace())): |
| 240 | + if (line[j] == '\t'): |
| 241 | + lineStart += 8 |
| 242 | + else: |
| 243 | + lineStart += 1 |
| 244 | + j += 1 |
| 245 | + # if the line contains only spaces, it doesn't count {{{ |
| 246 | + if (j == len(line)): |
| 247 | + continue |
| 248 | + # }}} |
| 249 | + # if the next character is # (python comment), this line doesn't count {{{ |
| 250 | + if (line[j] == '#'): |
| 251 | + continue |
| 252 | + # }}} |
| 253 | + # }}} |
| 254 | + # if the line's indentation starts before the nearest tag's one, the tag is wrong {{{ |
| 255 | + if (lineStart < tags[nearestLineNumber][3]): |
| 256 | + nearestLineNumber = -1 |
| 257 | + break |
| 258 | + # }}} |
| 259 | + # }}} |
| 260 | + # }}} |
| 261 | + else: |
| 262 | + nearestLineNumber = -1 |
| 263 | + # }}} |
| 264 | + |
| 265 | + # describe the cursor position (what tag it's in) {{{ |
| 266 | + tagDescription = "" |
| 267 | + if (nearestLineNumber > -1): |
| 268 | + tagInfo = tags[nearestLineNumber] |
| 269 | + # use the owner if any exists {{{ |
| 270 | + if (tagInfo[2] != None): |
| 271 | + fullTagName = "%s.%s()" % (tagInfo[2].split(':')[1], tagInfo[0],) |
| 272 | + # }}} |
| 273 | + # otherwise use just the tag name {{{ |
| 274 | + else: |
| 275 | + fullTagName = tagInfo[0] |
| 276 | + # }}} |
| 277 | + tagDescription = "[in %s (%s)]" % (fullTagName, tagInfo[1],) |
| 278 | + # }}} |
| 279 | + |
| 280 | + # update the variable for the status line so it will be updated next time |
| 281 | + vim.command("let w:PHStatusLine=\"%s\"" % (tagDescription,)) |
| 282 | + except: |
| 283 | + # spit out debugging information {{{ |
| 284 | + ec, ei, tb = sys.exc_info() |
| 285 | + while (tb != None): |
| 286 | + if (tb.tb_next == None): |
| 287 | + break |
| 288 | + tb = tb.tb_next |
| 289 | + print "ERROR: %s %s %s:%u" % (ec.__name__, ei, tb.tb_frame.f_code.co_filename, tb.tb_lineno,) |
| 290 | + time.sleep(0.5) |
| 291 | + # }}} |
| 292 | + # }}} |
| 293 | + |
| 294 | + |
| 295 | +def deleteTags(bufferNumber): |
| 296 | + # DOC {{{ |
| 297 | + """Removes tags data for the specified buffer number. |
| 298 | + |
| 299 | + Parameters |
| 300 | + |
| 301 | + bufferNumber -- number of the buffer |
| 302 | + """ |
| 303 | + # }}} |
| 304 | + |
| 305 | + # CODE {{{ |
| 306 | + global TAGS, TAGLINENUMBERS, BUFFERTICKS |
| 307 | + |
| 308 | + try: |
| 309 | + del TAGS[bufferNumber] |
| 310 | + del TAGLINENUMBERS[bufferNumber] |
| 311 | + del BUFFERTICKS[bufferNumber] |
| 312 | + except: |
| 313 | + pass |
| 314 | + # }}} |
| 315 | + |
| 316 | + |
| 317 | +EOS |
| 318 | + |
| 319 | + |
| 320 | +function! PHCursorHold() |
| 321 | + " only python is supported {{{ |
| 322 | + if (exists('b:current_syntax') && (b:current_syntax != 'python')) |
| 323 | + let w:PHStatusLine = '' |
| 324 | + return |
| 325 | + endif |
| 326 | + " }}} |
| 327 | + |
| 328 | + " call python function findTag() with the current buffer number and changed ticks |
| 329 | + execute 'python findTag(' . expand("<abuf>") . ', ' . b:changedtick . ')' |
| 330 | +endfunction |
| 331 | + |
| 332 | + |
| 333 | +function! PHBufferDelete() |
| 334 | + " call python function deleteTags() with the cur |
| 335 | + execute 'python deleteTags(' . expand("<abuf>") . ')' |
| 336 | +endfunction |
| 337 | + |
| 338 | + |
| 339 | + |
| 340 | +" autocommands binding |
| 341 | +autocmd CursorHold * silent call PHCursorHold() |
| 342 | +autocmd BufWinEnter * silent call PHCursorHold() |
| 343 | +autocmd BufDelete * silent call PHBufferDelete() |
| 344 | + |
| 345 | +" time that determines after how long time of no activity the CursorHold event |
| 346 | +" is fired up |
| 347 | +set updatetime=1000 |
| 348 | + |
| 349 | +" color of the current tag in the status line (bold cyan on black) |
| 350 | +highlight User1 gui=bold guifg=cyan guibg=black |
| 351 | +" color of the modified flag in the status line (bold black on red) |
| 352 | +highlight User2 gui=bold guifg=black guibg=red |
| 353 | +" the status line will be displayed for every window |
| 354 | +set laststatus=2 |
| 355 | +" set the status variable for the current window |
| 356 | +let w:PHStatusLine = '' |
| 357 | +" set the status line to display some useful information |
| 358 | +set stl=%-f%r\ %2*%m%*\ \ \ \ %1*%{w:PHStatusLine}%*%=[%l:%c]\ \ \ \ [buf\ %n] |
0 commit comments