mirror of
https://github.com/davidhalter/jedi-vim.git
synced 2025-12-08 03:24:47 +08:00
Revisit usage highlighting (#851)
This commit is contained in:
@@ -134,6 +134,58 @@ finally:
|
||||
sys.path.remove(parso_path)
|
||||
|
||||
|
||||
class VimCompat:
|
||||
_eval_cache = {}
|
||||
_func_cache = {}
|
||||
|
||||
@classmethod
|
||||
def has(cls, what):
|
||||
try:
|
||||
return cls._eval_cache[what]
|
||||
except KeyError:
|
||||
ret = cls._eval_cache[what] = cls.call('has', what)
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
def call(cls, func, *args):
|
||||
try:
|
||||
f = cls._func_cache[func]
|
||||
except KeyError:
|
||||
if IS_NVIM:
|
||||
f = cls._func_cache[func] = getattr(vim.funcs, func)
|
||||
else:
|
||||
f = cls._func_cache[func] = vim.Function(func)
|
||||
return f(*args)
|
||||
|
||||
@classmethod
|
||||
def setqflist(cls, items, title, context):
|
||||
if cls.has('patch-7.4.2200'): # can set qf title.
|
||||
what = {'title': title}
|
||||
if cls.has('patch-8.0.0590'): # can set qf context
|
||||
what['context'] = {'jedi_usages': context}
|
||||
if cls.has('patch-8.0.0657'): # can set items via "what".
|
||||
what['items'] = items
|
||||
cls.call('setqflist', [], ' ', what)
|
||||
else:
|
||||
# Can set title (and maybe context), but needs two calls.
|
||||
cls.call('setqflist', items)
|
||||
cls.call('setqflist', items, 'a', what)
|
||||
else:
|
||||
cls.call('setqflist', items)
|
||||
|
||||
@classmethod
|
||||
def setqflist_title(cls, title):
|
||||
if cls.has('patch-7.4.2200'):
|
||||
cls.call('setqflist', [], 'a', {'title': title})
|
||||
|
||||
@classmethod
|
||||
def can_update_current_qflist_for_context(cls, context):
|
||||
if cls.has('patch-8.0.0590'): # can set qf context
|
||||
return cls.call('getqflist', {'context': 1})['context'] == {
|
||||
'jedi_usages': context,
|
||||
}
|
||||
|
||||
|
||||
def catch_and_print_exceptions(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
@@ -345,7 +397,7 @@ def goto(mode="goto"):
|
||||
repr(PythonToVimStr(old_wildignore)))
|
||||
vim.current.window.cursor = d.line, d.column
|
||||
else:
|
||||
show_goto_multi_results(definitions)
|
||||
show_goto_multi_results(definitions, mode)
|
||||
return definitions
|
||||
|
||||
|
||||
@@ -370,9 +422,14 @@ def annotate_description(d):
|
||||
return '[%s] %s' % (typ, code)
|
||||
|
||||
|
||||
def show_goto_multi_results(definitions):
|
||||
"""Create a quickfix list for multiple definitions."""
|
||||
def show_goto_multi_results(definitions, mode):
|
||||
"""Create (or reuse) a quickfix list for multiple definitions."""
|
||||
global _current_definitions
|
||||
|
||||
lst = []
|
||||
(row, col) = vim.current.window.cursor
|
||||
current_idx = None
|
||||
current_def = None
|
||||
for d in definitions:
|
||||
if d.column is None:
|
||||
# Typically a namespace, in the future maybe other things as
|
||||
@@ -383,8 +440,47 @@ def show_goto_multi_results(definitions):
|
||||
lst.append(dict(filename=PythonToVimStr(relpath(d.module_path)),
|
||||
lnum=d.line, col=d.column + 1,
|
||||
text=PythonToVimStr(text)))
|
||||
vim_eval('setqflist(%s)' % repr(lst))
|
||||
vim_eval('jedi#add_goto_window(' + str(len(lst)) + ')')
|
||||
|
||||
# Select current/nearest entry via :cc later.
|
||||
if d.line == row and d.column <= col:
|
||||
if (current_idx is None
|
||||
or (abs(lst[current_idx].column - col)
|
||||
> abs(d.column - col))):
|
||||
current_idx = len(lst)
|
||||
current_def = d
|
||||
|
||||
# Build qflist title.
|
||||
qf_title = mode
|
||||
if current_def is not None:
|
||||
qf_title += ": " + current_def.full_name
|
||||
select_entry = current_idx
|
||||
else:
|
||||
select_entry = 0
|
||||
|
||||
qf_context = id(definitions)
|
||||
if (_current_definitions
|
||||
and VimCompat.can_update_current_qflist_for_context(qf_context)):
|
||||
# Same list, only adjust title/selected entry.
|
||||
VimCompat.setqflist_title(qf_title)
|
||||
else:
|
||||
VimCompat.setqflist(lst, title=qf_title, context=qf_context)
|
||||
for_usages = mode == "usages"
|
||||
vim_eval('jedi#add_goto_window(%d, %d)' % (for_usages, len(lst)))
|
||||
|
||||
vim_command('%dcc' % select_entry)
|
||||
|
||||
|
||||
def _same_definitions(a, b):
|
||||
"""Compare without _inference_state.
|
||||
|
||||
Ref: https://github.com/davidhalter/jedi-vim/issues/952)
|
||||
"""
|
||||
return all(
|
||||
x._name.start_pos == y._name.start_pos
|
||||
and x.module_path == y.module_path
|
||||
and x.name == y.name
|
||||
for x, y in zip(a, b)
|
||||
)
|
||||
|
||||
|
||||
@catch_and_print_exceptions
|
||||
@@ -396,22 +492,215 @@ def usages(visuals=True):
|
||||
return definitions
|
||||
|
||||
if visuals:
|
||||
highlight_usages(definitions)
|
||||
show_goto_multi_results(definitions)
|
||||
global _current_definitions
|
||||
|
||||
if _current_definitions:
|
||||
if _same_definitions(_current_definitions, definitions):
|
||||
definitions = _current_definitions
|
||||
else:
|
||||
clear_usages()
|
||||
assert not _current_definitions
|
||||
|
||||
show_goto_multi_results(definitions, "usages")
|
||||
if not _current_definitions:
|
||||
_current_definitions = definitions
|
||||
highlight_usages()
|
||||
else:
|
||||
assert definitions is _current_definitions # updated above
|
||||
return definitions
|
||||
|
||||
|
||||
def highlight_usages(definitions, length=None):
|
||||
for definition in definitions:
|
||||
# Only color the current module/buffer.
|
||||
if (definition.module_path or '') == vim.current.buffer.name:
|
||||
# mathaddpos needs a list of positions where a position is a list
|
||||
# of (line, column, length).
|
||||
# The column starts with 1 and not 0.
|
||||
positions = [
|
||||
[definition.line, definition.column + 1, length or len(definition.name)]
|
||||
]
|
||||
vim_eval("matchaddpos('jediUsage', %s)" % repr(positions))
|
||||
_current_definitions = None
|
||||
"""Current definitions to use for highlighting."""
|
||||
_pending_definitions = {}
|
||||
"""Pending definitions for unloaded buffers."""
|
||||
_placed_definitions_in_buffers = set()
|
||||
"""Set of buffers for faster cleanup."""
|
||||
|
||||
|
||||
IS_NVIM = hasattr(vim, 'from_nvim')
|
||||
if IS_NVIM:
|
||||
vim_prop_add = None
|
||||
else:
|
||||
vim_prop_type_added = False
|
||||
try:
|
||||
vim_prop_add = vim.Function("prop_add")
|
||||
except ValueError:
|
||||
vim_prop_add = None
|
||||
else:
|
||||
vim_prop_remove = vim.Function("prop_remove")
|
||||
|
||||
|
||||
def clear_usages():
|
||||
"""Clear existing highlights."""
|
||||
global _current_definitions
|
||||
if _current_definitions is None:
|
||||
return
|
||||
_current_definitions = None
|
||||
|
||||
if IS_NVIM:
|
||||
for buf in _placed_definitions_in_buffers:
|
||||
src_ids = buf.vars.get('_jedi_usages_src_ids')
|
||||
if src_ids is not None:
|
||||
for src_id in src_ids:
|
||||
buf.clear_highlight(src_id)
|
||||
elif vim_prop_add:
|
||||
for buf in _placed_definitions_in_buffers:
|
||||
vim_prop_remove({
|
||||
'type': 'jediUsage',
|
||||
'all': 1,
|
||||
'bufnr': buf.number,
|
||||
})
|
||||
else:
|
||||
# Unset current window only.
|
||||
assert _current_definitions is None
|
||||
highlight_usages_for_vim_win()
|
||||
|
||||
_placed_definitions_in_buffers.clear()
|
||||
|
||||
|
||||
def highlight_usages():
|
||||
"""Set definitions to be highlighted.
|
||||
|
||||
With Neovim it will use the nvim_buf_add_highlight API to highlight all
|
||||
buffers already.
|
||||
|
||||
With Vim without support for text-properties only the current window is
|
||||
highlighted via matchaddpos, and autocommands are setup to highlight other
|
||||
windows on demand. Otherwise Vim's text-properties are used.
|
||||
"""
|
||||
global _current_definitions, _pending_definitions
|
||||
|
||||
definitions = _current_definitions
|
||||
_pending_definitions = {}
|
||||
|
||||
if IS_NVIM or vim_prop_add:
|
||||
bufs = {x.name: x for x in vim.buffers}
|
||||
defs_per_buf = {}
|
||||
for definition in definitions:
|
||||
try:
|
||||
buf = bufs[definition.module_path]
|
||||
except KeyError:
|
||||
continue
|
||||
defs_per_buf.setdefault(buf, []).append(definition)
|
||||
|
||||
if IS_NVIM:
|
||||
# We need to remember highlight ids with Neovim's API.
|
||||
buf_src_ids = {}
|
||||
for buf, definitions in defs_per_buf.items():
|
||||
buf_src_ids[buf] = []
|
||||
for definition in definitions:
|
||||
src_id = _add_highlight_definition(buf, definition)
|
||||
buf_src_ids[buf].append(src_id)
|
||||
for buf, src_ids in buf_src_ids.items():
|
||||
buf.vars['_jedi_usages_src_ids'] = src_ids
|
||||
else:
|
||||
for buf, definitions in defs_per_buf.items():
|
||||
try:
|
||||
for definition in definitions:
|
||||
_add_highlight_definition(buf, definition)
|
||||
except vim.error as exc:
|
||||
if exc.args[0].startswith('Vim:E275:'):
|
||||
# "Cannot add text property to unloaded buffer"
|
||||
_pending_definitions.setdefault(buf.name, []).extend(
|
||||
definitions)
|
||||
else:
|
||||
highlight_usages_for_vim_win()
|
||||
|
||||
|
||||
def _handle_pending_usages_for_buf():
|
||||
"""Add (pending) highlights for the current buffer (Vim with textprops)."""
|
||||
buf = vim.current.buffer
|
||||
bufname = buf.name
|
||||
try:
|
||||
buf_defs = _pending_definitions[bufname]
|
||||
except KeyError:
|
||||
return
|
||||
for definition in buf_defs:
|
||||
_add_highlight_definition(buf, definition)
|
||||
del _pending_definitions[bufname]
|
||||
|
||||
|
||||
def _add_highlight_definition(buf, definition):
|
||||
lnum = definition.line
|
||||
start_col = definition.column
|
||||
|
||||
# Skip highlighting of module definitions that point to the start
|
||||
# of the file.
|
||||
if definition.type == 'module' and lnum == 1 and start_col == 0:
|
||||
return
|
||||
|
||||
_placed_definitions_in_buffers.add(buf)
|
||||
|
||||
# TODO: validate that definition.name is at this position?
|
||||
# Would skip the module definitions from above already.
|
||||
|
||||
length = len(definition.name)
|
||||
if vim_prop_add:
|
||||
# XXX: needs jediUsage highlight (via after/syntax/python.vim).
|
||||
global vim_prop_type_added
|
||||
if not vim_prop_type_added:
|
||||
vim.eval("prop_type_add('jediUsage', {'highlight': 'jediUsage'})")
|
||||
vim_prop_type_added = True
|
||||
vim_prop_add(lnum, start_col+1, {
|
||||
'type': 'jediUsage',
|
||||
'bufnr': buf.number,
|
||||
'length': length,
|
||||
})
|
||||
return
|
||||
|
||||
assert IS_NVIM
|
||||
end_col = definition.column + length
|
||||
src_id = buf.add_highlight('jediUsage', lnum-1, start_col, end_col,
|
||||
src_id=0)
|
||||
return src_id
|
||||
|
||||
|
||||
def highlight_usages_for_vim_win():
|
||||
"""Highlight usages in the current window.
|
||||
|
||||
It stores the matchids in a window-local variable.
|
||||
|
||||
(matchaddpos() only works for the current window.)
|
||||
"""
|
||||
global _current_definitions
|
||||
definitions = _current_definitions
|
||||
|
||||
win = vim.current.window
|
||||
|
||||
cur_matchids = win.vars.get('_jedi_usages_vim_matchids')
|
||||
if cur_matchids:
|
||||
if cur_matchids[0] == vim.current.buffer.number:
|
||||
return
|
||||
|
||||
# Need to clear non-matching highlights.
|
||||
for matchid in cur_matchids[1:]:
|
||||
expr = 'matchdelete(%d)' % int(matchid)
|
||||
vim.eval(expr)
|
||||
|
||||
matchids = []
|
||||
if definitions:
|
||||
buffer_path = vim.current.buffer.name
|
||||
for definition in definitions:
|
||||
if (definition.module_path or '') == buffer_path:
|
||||
positions = [
|
||||
[definition.line,
|
||||
definition.column + 1,
|
||||
len(definition.name)]
|
||||
]
|
||||
expr = "matchaddpos('jediUsage', %s)" % repr(positions)
|
||||
matchids.append(int(vim_eval(expr)))
|
||||
|
||||
if matchids:
|
||||
vim.current.window.vars['_jedi_usages_vim_matchids'] = [
|
||||
vim.current.buffer.number] + matchids
|
||||
elif cur_matchids is not None:
|
||||
# Always set it (uses an empty list for "unset", which is not possible
|
||||
# using del).
|
||||
vim.current.window.vars['_jedi_usages_vim_matchids'] = []
|
||||
|
||||
# Remember if clearing is needed for later buffer autocommands.
|
||||
vim.current.buffer.vars['_jedi_usages_needs_clear'] = bool(matchids)
|
||||
|
||||
|
||||
@_check_jedi_availability(show_error=True)
|
||||
|
||||
Reference in New Issue
Block a user