fast-tags-2.0.1: tools/qualified_tag.py
# Replace the ^] follow tag command with one that expands a word without
# modifying iskeyword. Setting iskeyword is a lot simpler, but it also
# changes the behavoiour of a lot of vim commands, such as w, which is too
# confusing for me. This can also do clever things like allow ' in
# identifiers, but strip quotes from haddock references like 'A.B'.
#
# This assumes you are using fast-tags --fully_qualified, since it adds
# full module qualification to qualified tags, after renaming them based on
# the 'import ... as XYZ' lines in this file.
#
# I install it into ~/.vim/py and enable like this:
#
# In global .vimrc:
#
# if has('python')
# py import sys, os, vim
# py sys.path.insert(0, os.environ['HOME'] + '/.vim/py')
# endif
#
# In your haskell specific vimrc:
#
# if has('python')
# py import qualified_tag
# nnoremap <buffer> <silent> <c-]> :py qualified_tag.tag_word(vim)<cr>
# nnoremap <buffer> <silent> <c-Bslash> :py qualified_tag.tag_preview(vim)<cr>
# endif
#
# Substitute has('python3') and 'py3' if your vim uses python3.
#
# This also adds ^\ to preview a tag in a quickfix window. I use this to get
# type signatures, haddocks, and parameter names, or data declarations.
#
# If you use 'filetype', you can do:
#
# autocmd FileType haskell nnoremap ...
from __future__ import print_function
import sys, re
### preview tag
def tag_preview(vim):
"""This abuses vim's quickfix window to show a preview of where the
tag will go.
"""
matches, _word = get_tag(vim)
if not matches:
return
match = matches[0]
linenumber = int(match['cmd'])
i, lines = get_preview(match['kind'], match['filename'], linenumber)
# This is sketchy, since it only works if python repr is valid viml, but
# "sketchy" is normal for vim. Speaking of which, for some unknown
# reason, | is a special character when appearing in expressions, but
# only for :cgetexpr, not for e.g. :echo.
vim.command('cgetexpr ' + repr(lines).replace('|', '\\|'))
vim.command('copen ' + repr(len(lines)))
vim.command('cc ' + str(i+1))
# TODO Set title to file:linenumber, or file.match['name']
# Go to previous window, to get out of the qf window.
vim.command('wincmd p')
def get_preview(kind, fname, linenumber):
fp = open(fname)
lines = fp.readlines()
fp.close()
before, after = kind_context.get(kind, context_generic)(
lines, linenumber-1)
return len(before), list(map(str.rstrip, before + after))
def context_type(lines, i):
before = back_until_haddock(lines, i)
after = forward_until(lines, i, 5, ['}', 'deriving'], dedent=True)
return before, after
def context_function(lines, i):
# The tag should be on the line with ::, but maybe not.
before = back_until_haddock(lines, i)
after = forward_until(lines, i, 3, ['='])
return before, after
def context_generic(lines, i):
return lines[i-1 : i], forward_until(lines, i, 3, [], dedent=True)
def truncate(maxlen, xs):
if len(xs) > maxlen:
return xs[:maxlen] + ['...']
else:
return xs
def back_until_haddock(lines, start):
maxlines = 5
i = start
while i > 0:
if not lines[i].strip():
i += 1
break
elif lines[i].startswith(('-- |', '{- |')):
break
else:
i -= 1
return truncate(maxlines, lines[i : start])
def forward_until(lines, start, maxlines, infixes, dedent=False):
i = start
indented = False
while i < len(lines) and i - start < maxlines:
if not lines[i].strip():
i -= 1
break
elif dedent and indented and not lines[i][0].isspace():
# A dedented after an indent must be the next definition.
i -= 1
break
else:
ws = lines[i].split()
if any(infix in ws for infix in infixes):
break
else:
if lines[i][0].isspace():
indented = True
i += 1
return lines[start : i+1]
kind_context = {
'm': context_generic, # module
'f': context_function, # function
'c': context_type, # class
't': context_type, # type
'C': context_type, # constructor
'o': context_function, # function
'p': context_generic, # pattern * = ...
'F': context_generic, # type family * = ...
'D': context_generic, # define
}
### follow tag
def tag_word(vim):
matches, word = get_tag(vim)
if not matches:
# I'd use echoerr, but that causes a big messy python traceback.
vim.command('echohl ErrorMsg | echo %r | echohl None'
% ('tag not found: ' + word,))
else:
vim.command('tag ' + word)
def get_tag(vim):
"""Get the word under the cursor, return
[str] - Exact names for tags matching the word.
str - Word from the cursor.
"""
word = get_word(vim)
qual_to_module = get_qualified_imports(vim.current.buffer)
tag = guess_tag(qual_to_module, word)
# If I can't find a qualified target, try it without the qualification.
# It might be a re-export from another module.
matches = has_target(vim, tag)
if not matches and '.' in tag:
tag = tag.split('.')[-1]
matches = has_target(vim, tag)
return matches, tag
def has_target(vim, tag):
return vim.eval('taglist(%r)' % ('^' + tag + '$',))
def get_word(vim):
(row, col) = vim.current.window.cursor
line = vim.current.buffer[row-1]
return expand_word(line, col)
def expand_word(line, col):
# This is broken for unicode, but vim gives the col index in bytes, and
# python gives no way to convert byte index to unicode index, short of
# decoding myself. And even if I did, python uses UTF16 so it would
# still be wrong for unicode above U+FFFF.
start = col
while start > 0 and is_keyword(line[start-1]):
start -= 1
end = col
while end < len(line) and is_keyword(line[end]):
end += 1
word = line[start:end]
# Strip punctuation in case it's in a sentence in a comment.
word = word.strip('.')
if word and (word[0] == word[-1] == "'" or word[0] == word[-1] == '"'):
# Make tags on haddock references like 'A.B' work.
return word[1:-1]
else:
return word
def is_keyword(c):
return c.isalnum() or c in "'._"
def guess_tag(qual_to_module, word):
if '.' not in word:
return word
components = word.split('.')
qual = '.'.join(components[:-1])
if qual in qual_to_module:
return qual_to_module[qual] + '.' + components[-1]
else:
return word
def get_qualified_imports(lines):
imports = get_imports(lines)
qual_to_module = {}
for m in re.finditer(r'\b([A-Za-z0-9.]+)\s+as\s+([A-Za-z00-9.]+)$',
imports, re.MULTILINE):
qual_to_module[m.group(2)] = m.group(1)
return qual_to_module
def get_imports(lines):
imports = []
in_imports = False
for line in lines:
if line.startswith('import'):
in_imports = True
if in_imports:
if finished_imports(line):
break
imports.append(line)
return '\n'.join(imports)
def finished_imports(line):
# Hacky heuristic to see if I'm past the import block.
# If column 0 isn't 'import' or a space or a comment.
return line and not (
line.startswith('import') or line[0].isspace()
or line.startswith('--')
)
def test():
fn, tag = sys.argv[1:]
lines = open(fn).read().split('\n')
qual_to_module = get_qualified_imports(lines)
print(qual_to_module)
print(guess_tag(qual_to_module, tag))
if __name__ == '__main__':
test()