1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-01-24 09:20:06 +08:00
SpaceVim/bundle/defx.nvim/rplugin/python3/defx/view.py
2020-10-31 15:58:52 +08:00

835 lines
30 KiB
Python

# ============================================================================
# FILE: view.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
import copy
import time
import typing
from pynvim.api import Buffer
from pathlib import Path
from defx.clipboard import Clipboard
from defx.context import Context
from defx.defx import Defx
from defx.session import Session
from defx.util import error, import_plugin, safe_call, len_bytes
from defx.util import Nvim, Candidate
Highlights = typing.List[typing.Tuple[str, int, int]]
class View(object):
def __init__(self, vim: Nvim, index: int) -> None:
self._vim: Nvim = vim
self._defxs: typing.List[Defx] = []
self._candidates: typing.List[typing.Dict[str, typing.Any]] = []
self._clipboard = Clipboard()
self._bufnr = -1
self._prev_bufnr = -1
self._winid = -1
self._index = index
self._bufname = '[defx]'
self._buffer: Buffer = None
self._prev_action = ''
self._prev_syntaxes: typing.List[str] = []
self._prev_highlight_commands: typing.List[str] = []
self._winrestcmd = ''
self._has_preview_window = False
self._session_version = '1.0'
self._sessions: typing.Dict[str, Session] = {}
self._previewed_target: typing.Optional[Candidate] = None
self._previewed_job: typing.Optional[int] = None
self._ns: int = -1
self._has_textprop = False
self._proptypes: typing.Set[str] = set()
def init(self, context: typing.Dict[str, typing.Any]) -> None:
self._context = self._init_context(context)
self._bufname = f'[defx] {self._context.buffer_name}-{self._index}'
self._winrestcmd = self._vim.call('winrestcmd')
self._prev_wininfo = self._get_wininfo()
self._prev_bufnr = self._context.prev_bufnr
self._has_preview_window = len(
[x for x in range(1, self._vim.call('winnr', '$'))
if self._vim.call('getwinvar', x, '&previewwindow')]) > 0
if self._vim.call('defx#util#has_textprop'):
self._has_textprop = True
else:
self._ns = self._vim.call('nvim_create_namespace', 'defx')
def init_paths(self, paths: typing.List[typing.List[str]],
context: typing.Dict[str, typing.Any],
clipboard: Clipboard
) -> bool:
self.init(context)
initialized = self._init_defx(clipboard)
# Window check
if self._vim.call('win_getid') != self._winid:
# Not defx window
return False
if not paths:
if not initialized:
# Don't initialize path
return False
paths = [['file', self._vim.call('getcwd')]]
self._buffer.vars['defx']['paths'] = paths
self._update_defx_paths(paths)
self._init_columns(self._context.columns.split(':'))
self.redraw(True)
if self._context.session_file:
self.do_action('load_session', [],
self._vim.call('defx#init#_context', {}))
for [index, [source_name, path]] in enumerate(paths):
self._check_session(index, path)
for defx in self._defxs:
self._init_cursor(defx)
return True
def do_action(self, action_name: str,
action_args: typing.List[str],
new_context: typing.Dict[str, typing.Any]) -> None:
"""
Do "action" action.
"""
cursor = new_context['cursor']
visual_start = new_context['visual_start']
visual_end = new_context['visual_end']
defx_targets = {
x._index: self.get_selected_candidates(cursor, x._index)
for x in self._defxs}
all_targets: typing.List[typing.Dict[str, typing.Any]] = []
for targets in defx_targets.values():
all_targets += targets
import defx.action as action
for defx in [x for x in self._defxs
if not all_targets or defx_targets[x._index]]:
context = self._context._replace(
args=action_args,
cursor=cursor,
targets=defx_targets[defx._index],
visual_start=visual_start,
visual_end=visual_end,
)
ret = action.do_action(self, defx, action_name, context)
if ret:
error(self._vim, 'Invalid action_name:' + action_name)
return
def debug(self, expr: typing.Any) -> None:
error(self._vim, expr)
def print_msg(self, expr: typing.Any) -> None:
self._vim.call('defx#util#print_message', expr)
def close_preview(self) -> None:
if not self._has_preview_window:
self._vim.command('pclose!')
# Clear previewed buffers
for bufnr in self._vim.vars['defx#_previewed_buffers'].keys():
if not self._vim.call('win_findbuf', bufnr):
self._vim.command('silent bdelete ' + str(bufnr))
self._vim.vars['defx#_previewed_buffers'] = {}
def quit(self) -> None:
# Close preview window
self.close_preview()
winnr = self._vim.call('bufwinnr', self._bufnr)
if winnr < 0:
return
if winnr != self._vim.call('winnr'):
# Use current window
self._context = self._context._replace(
prev_winid=self._vim.call('win_getid'))
self._vim.command(f'{winnr}wincmd w')
if (self._context.split not in ['no', 'tab'] and
self._vim.call('winnr', '$') != 1):
self._vim.command('close')
self._vim.call('win_gotoid', self._context.prev_winid)
elif self._check_bufnr(self._prev_bufnr):
self._vim.command('buffer ' + str(self._prev_bufnr))
elif self._check_bufnr(self._context.prev_last_bufnr):
self._vim.command('buffer ' +
str(self._context.prev_last_bufnr))
else:
self._vim.command('enew')
if self._get_wininfo() and self._get_wininfo() == self._prev_wininfo:
self._vim.command(self._winrestcmd)
self.restore_previous_buffer()
def redraw(self, is_force: bool = False) -> None:
"""
Redraw defx buffer.
"""
start = time.time()
[info] = self._vim.call('getbufinfo', self._bufnr)
prev_linenr = info['lnum']
prev = self.get_cursor_candidate(prev_linenr)
if is_force:
self._init_candidates()
self._init_column_length()
for column in self._columns:
column.on_redraw(self, self._context)
lines = []
columns_highlights = []
for (i, candidate) in enumerate(self._candidates):
(text, highlights) = self._get_columns_text(
self._context, candidate)
lines.append(text)
columns_highlights += ([(x[0], i, x[1], x[1] + x[2])
for x in highlights])
self._buffer.options['modifiable'] = True
# NOTE: Different len of buffer line replacement cause cursor jump
if len(lines) >= len(self._buffer):
self._buffer[:] = lines[:len(self._buffer)]
self._buffer.append(lines[len(self._buffer):])
else:
self._buffer[len(lines):] = []
self._buffer[:] = lines
self._buffer.options['modifiable'] = False
self._buffer.options['modified'] = False
# TODO: How to set cursor position for other buffer when
# stay in current buffer
if self._buffer == self._vim.current.buffer:
self._vim.call('cursor', [prev_linenr, 0])
if prev:
self.search_file(prev['action__path'], prev['_defx_index'])
if is_force:
self._init_column_syntax()
# Update highlights
# Note: update_highlights() must be called after init_column_syntax()
if columns_highlights:
self._update_highlights(columns_highlights)
if self._context.profile:
error(self._vim, f'redraw time = {time.time() - start}')
def get_cursor_candidate(
self, cursor: int) -> typing.Dict[str, typing.Any]:
if len(self._candidates) < cursor:
return {}
else:
return self._candidates[cursor - 1]
def get_selected_candidates(
self, cursor: int, index: int
) -> typing.List[typing.Dict[str, typing.Any]]:
if not self._candidates:
return []
candidates = [x for x in self._candidates if x['is_selected']]
if not candidates:
candidates = [self.get_cursor_candidate(cursor)]
return [x for x in candidates if x.get('_defx_index', -1) == index]
def get_candidate_pos(self, path: Path, index: int) -> int:
for [pos, candidate] in enumerate(self._candidates):
if (candidate['_defx_index'] == index and
candidate['action__path'] == path):
return pos
return -1
def cd(self, defx: Defx, source_name: str,
path: str, cursor: int) -> None:
history = defx._cursor_history
# Save previous cursor position
candidate = self.get_cursor_candidate(cursor)
if candidate:
history[defx._cwd] = candidate['action__path']
global_histories = self._vim.vars['defx#_histories']
global_histories.append([defx._source.name, defx._cwd])
self._vim.vars['defx#_histories'] = global_histories
if source_name != defx._source.name:
# Replace with new defx
self._defxs[defx._index] = Defx(self._vim, self._context,
source_name, path, defx._index)
defx = self._defxs[defx._index]
defx.cd(path)
self.redraw(True)
self._check_session(defx._index, path)
self._init_cursor(defx)
if path in history:
self.search_file(history[path], defx._index)
self._update_paths(defx._index, path)
def search_file(self, path: Path, index: int) -> bool:
target = str(path)
if target and target[-1] == '/':
target = target[:-1]
pos = self.get_candidate_pos(Path(target), index)
if pos < 0:
return False
self._vim.call('cursor', [pos + 1, 1])
return True
def search_recursive(self, path: Path, index: int) -> None:
parents: typing.List[Path] = []
tmppath: Path = path
while (self.get_candidate_pos(tmppath, index) < 0 and
tmppath.parent != path and tmppath.parent != tmppath):
tmppath = tmppath.parent
parents.append(tmppath)
for parent in reversed(parents):
self.open_tree(parent, index, False, 0)
self.update_candidates()
self.redraw()
self.search_file(path, index)
def update_candidates(self) -> None:
# Update opened/selected state
for defx in self._defxs:
defx._opened_candidates = set()
defx._selected_candidates = set()
for [i, candidate] in [x for x in enumerate(self._candidates)
if x[1]['is_opened_tree']]:
defx = self._defxs[candidate['_defx_index']]
defx._opened_candidates.add(str(candidate['action__path']))
for [i, candidate] in [x for x in enumerate(self._candidates)
if x[1]['is_selected']]:
defx = self._defxs[candidate['_defx_index']]
defx._selected_candidates.add(str(candidate['action__path']))
def open_tree(self, path: Path, index: int, enable_nested: bool,
max_level: int = 0) -> None:
# Search insert position
pos = self.get_candidate_pos(path, index)
if pos < 0:
return
target = self._candidates[pos]
if (not target['is_directory'] or
target['is_opened_tree'] or target['is_root']):
return
target['is_opened_tree'] = True
base_level = target['level'] + 1
defx = self._defxs[index]
children = defx.gather_candidates_recursive(
str(path), base_level, base_level + max_level)
if not children:
return
if (enable_nested and len(children) == 1
and children[0]['is_directory']):
# Merge child.
defx._nested_candidates.add(str(target['action__path']))
target['action__path'] = children[0]['action__path']
target['word'] += children[0]['word']
target['is_opened_tree'] = False
return self.open_tree(target['action__path'],
index, enable_nested, max_level)
for candidate in children:
candidate['_defx_index'] = index
self._candidates = (self._candidates[: pos + 1] +
children + self._candidates[pos + 1:])
def close_tree(self, path: Path, index: int) -> None:
# Search insert position
pos = self.get_candidate_pos(path, index)
if pos < 0:
return
target = self._candidates[pos]
if not target['is_opened_tree'] or target['is_root']:
return
target['is_opened_tree'] = False
defx = self._defxs[index]
self._remove_nested_path(defx, target['action__path'])
start = pos + 1
base_level = target['level']
end = start
for candidate in self._candidates[start:]:
if candidate['level'] <= base_level:
break
self._remove_nested_path(defx, candidate['action__path'])
end += 1
self._candidates = (self._candidates[: start] +
self._candidates[end:])
def restore_previous_buffer(self) -> None:
if not self._vim.call('buflisted', self._prev_bufnr):
return
prev_bufname = self._vim.call('bufname',
self._context.prev_last_bufnr)
if not prev_bufname:
# ignore noname buffer
return
self._vim.call('setreg', '#',
self._vim.call('fnameescape', prev_bufname))
def _remove_nested_path(self, defx: Defx, path: Path) -> None:
if str(path) in defx._nested_candidates:
defx._nested_candidates.remove(str(path))
def _init_context(
self, context: typing.Dict[str, typing.Any]) -> Context:
# Convert to int
for attr in [x[0] for x in Context()._asdict().items()
if isinstance(x[1], int) and x[0] in context]:
context[attr] = int(context[attr])
return Context(**context)
def _init_window(self) -> None:
self._winid = self._vim.call('win_getid')
window_options = self._vim.current.window.options
if (self._context.split == 'vertical'
and self._context.winwidth > 0):
window_options['winfixwidth'] = True
self._vim.command(f'vertical resize {self._context.winwidth}')
elif (self._context.split == 'horizontal' and
self._context.winheight > 0):
window_options['winfixheight'] = True
self._vim.command(f'resize {self._context.winheight}')
def _check_session(self, index: int, path: str) -> None:
if path not in self._sessions:
return
# restore opened_candidates
session = self._sessions[path]
for opened_path in session.opened_candidates:
self.open_tree(Path(opened_path), index, False)
self.update_candidates()
self.redraw()
def _init_defx(self, clipboard: Clipboard) -> bool:
if not self._switch_buffer():
return False
self._buffer = self._vim.current.buffer
self._bufnr = self._buffer.number
self._buffer.vars['defx'] = {
'context': self._context._asdict(),
'paths': [],
}
# Note: Have to use setlocal instead of "current.window.options"
# "current.window.options" changes global value instead of local in
# neovim.
self._vim.command('setlocal colorcolumn=')
self._vim.command('setlocal nocursorcolumn')
self._vim.command('setlocal nofoldenable')
self._vim.command('setlocal foldcolumn=0')
self._vim.command('setlocal nolist')
self._vim.command('setlocal nonumber')
self._vim.command('setlocal norelativenumber')
self._vim.command('setlocal nospell')
self._vim.command('setlocal nowrap')
self._vim.command('setlocal signcolumn=no')
if self._context.split == 'floating':
self._vim.command('setlocal nocursorline')
self._init_window()
buffer_options = self._buffer.options
if not self._context.listed:
buffer_options['buflisted'] = False
buffer_options['buftype'] = 'nofile'
buffer_options['bufhidden'] = 'hide'
buffer_options['swapfile'] = False
buffer_options['modeline'] = False
buffer_options['modifiable'] = False
buffer_options['modified'] = False
buffer_options['filetype'] = 'defx'
if not self._vim.call('has', 'nvim'):
# In Vim8, FileType autocmd is not fired after set filetype option.
self._vim.command('silent doautocmd FileType defx')
self._vim.command('autocmd! defx * <buffer>')
self._vim.command('autocmd defx '
'CursorHold,FocusGained <buffer> '
'call defx#call_async_action("check_redraw")')
self._vim.command('autocmd defx FileType <buffer> '
'call defx#call_action("redraw")')
self._prev_highlight_commands = []
# Initialize defx state
self._candidates = []
self._clipboard = clipboard
self._defxs = []
self._init_all_columns()
self._init_columns(self._context.columns.split(':'))
self._vim.vars['defx#_drives'] = self._context.drives
return True
def _switch_buffer(self) -> bool:
if self._context.split == 'tab':
self._vim.command('tabnew')
if self._context.close:
self.quit()
return False
winnr = self._vim.call('bufwinnr', self._bufnr)
if winnr > 0:
self._vim.command(f'{winnr}wincmd w')
if self._context.toggle:
self.quit()
else:
self._winid = self._vim.call('win_getid')
self._init_window()
return False
if (self._vim.current.buffer.options['modified'] and
not self._vim.options['hidden'] and
self._context.split == 'no'):
self._context = self._context._replace(split='vertical')
if (self._context.split == 'floating'
and self._vim.call('exists', '*nvim_open_win')):
# Use floating window
self._vim.call(
'nvim_open_win',
self._vim.call('bufnr', '%'), True, {
'relative': self._context.winrelative,
'row': self._context.winrow,
'col': self._context.wincol,
'width': self._context.winwidth,
'height': self._context.winheight,
})
# Create new buffer
vertical = 'vertical' if self._context.split == 'vertical' else ''
no_split = self._context.split in ['no', 'tab', 'floating']
if self._vim.call('bufloaded', self._bufnr):
command = ('buffer' if no_split else 'sbuffer')
self._vim.command(
'silent keepalt %s %s %s %s' % (
self._context.direction,
vertical,
command,
self._bufnr,
)
)
if self._context.resume:
self._init_window()
return False
elif self._vim.call('exists', 'bufadd'):
bufnr = self._vim.call('bufadd', self._bufname)
command = ('buffer' if no_split else 'sbuffer')
self._vim.command(
'silent keepalt %s %s %s %s' % (
self._context.direction,
vertical,
command,
bufnr,
)
)
else:
command = ('edit' if no_split else 'new')
self._vim.call(
'defx#util#execute_path',
'silent keepalt %s %s %s ' % (
self._context.direction,
vertical,
command,
),
self._bufname)
return True
def _init_all_columns(self) -> None:
from defx.base.column import Base as Column
self._all_columns: typing.Dict[str, Column] = {}
for path_column in self._load_custom_columns():
column = import_plugin(path_column, 'column', 'Column')
if not column:
continue
column = column(self._vim)
if column.name not in self._all_columns:
self._all_columns[column.name] = column
def _init_columns(self, columns: typing.List[str]) -> None:
from defx.base.column import Base as Column
custom = self._vim.call('defx#custom#_get')['column']
self._columns: typing.List[Column] = [
copy.copy(self._all_columns[x])
for x in columns if x in self._all_columns
]
for column in self._columns:
if column.name in custom:
column.vars.update(custom[column.name])
column.on_init(self, self._context)
def _init_column_length(self) -> None:
from defx.base.column import Base as Column
within_variable = False
within_variable_columns: typing.List[Column] = []
start = 1
for [index, column] in enumerate(self._columns):
column.syntax_name = f'Defx_{column.name}_{index}'
column.highlight_name = f'Defx_{column.name}'
if within_variable and not column.is_stop_variable:
within_variable_columns.append(column)
continue
# Calculate variable_length
variable_length = 0
if column.is_stop_variable:
for variable_column in within_variable_columns:
variable_length += variable_column.length(
self._context._replace(targets=self._candidates))
# Note: for "' '.join(variable_texts)" length
if within_variable_columns:
variable_length += len(within_variable_columns) - 1
length = column.length(
self._context._replace(targets=self._candidates,
variable_length=variable_length))
column.start = start
column.end = start + length
if column.is_start_variable:
within_variable = True
within_variable_columns.append(column)
else:
column.is_within_variable = False
start += length + 1
if column.is_stop_variable:
for variable_column in within_variable_columns:
# Overwrite syntax_name
variable_column.syntax_name = column.syntax_name
variable_column.is_within_variable = True
within_variable = False
def _init_column_syntax(self) -> None:
commands: typing.List[str] = []
for syntax in self._prev_syntaxes:
commands.append(
'silent! syntax clear ' + syntax)
if self._proptypes:
self._clear_prop_types()
self._prev_syntaxes = []
for column in self._columns:
source_highlights = column.highlight_commands()
if not source_highlights:
continue
commands += source_highlights
self._prev_syntaxes += column.syntaxes()
syntax_list = commands + [
self._vim.call('execute', 'syntax list'),
self._vim.call('execute', 'highlight'),
]
if syntax_list == self._prev_highlight_commands:
# Skip highlights
return
self._execute_commands(commands)
self._prev_highlight_commands = commands + [
self._vim.call('execute', 'syntax list'),
self._vim.call('execute', 'highlight'),
]
def _execute_commands(self, commands: typing.List[str]) -> None:
# Note: If commands are too huge, vim.command() will fail.
threshold = 15
cnt = 0
while cnt < len(commands):
self._vim.command(' | '.join(commands[cnt: cnt + threshold]))
cnt += threshold
def _init_candidates(self) -> None:
self._candidates = []
for defx in self._defxs:
root = defx.get_root_candidate()
defx._mtime = root['action__path'].stat().st_mtime
candidates = [root]
candidates += defx.tree_candidates(
defx._cwd, 0, self._context.auto_recursive_level)
for candidate in candidates:
candidate['_defx_index'] = defx._index
self._candidates += candidates
def _get_columns_text(self, context: Context, candidate: Candidate
) -> typing.Tuple[str, Highlights]:
texts: typing.List[str] = []
variable_texts: typing.List[str] = []
ret_highlights: typing.List[typing.Tuple[str, int, int]] = []
start = 0
for column in self._columns:
column.start = start
if column.is_stop_variable:
if variable_texts:
variable_texts.append('')
(text, highlights) = column.get_with_variable_text(
context, ' '.join(variable_texts), candidate)
texts.append(text)
ret_highlights += highlights
variable_texts = []
else:
if column.has_get_with_highlights:
(text, highlights) = column.get_with_highlights(
context, candidate)
ret_highlights += highlights
else:
# Note: For old columns compatibility
text = column.get(context, candidate)
if column.is_start_variable or column.is_within_variable:
if text:
variable_texts.append(text)
else:
texts.append(text)
start = len_bytes(' '.join(texts))
if texts:
start += 1
if variable_texts:
start += len_bytes(' '.join(variable_texts)) + 1
return (' '.join(texts), ret_highlights)
def _update_paths(self, index: int, path: str) -> None:
var_defx = self._buffer.vars['defx']
if len(var_defx['paths']) <= index:
var_defx['paths'].append(path)
else:
var_defx['paths'][index] = path
self._buffer.vars['defx'] = var_defx
def _init_cursor(self, defx: Defx) -> None:
self.search_file(Path(defx._cwd), defx._index)
# Move to next
self._vim.call('cursor', [self._vim.call('line', '.') + 1, 1])
def _get_wininfo(self) -> typing.List[str]:
return [
self._vim.options['columns'], self._vim.options['lines'],
self._vim.call('win_getid'), self._vim.call('tabpagebuflist')
]
def _load_custom_columns(self) -> typing.List[Path]:
rtp_list = self._vim.options['runtimepath'].split(',')
result: typing.List[Path] = []
for path in rtp_list:
column_path = Path(path).joinpath(
'rplugin', 'python3', 'defx', 'column')
if safe_call(column_path.is_dir):
result += column_path.glob('*.py')
return result
def _update_defx_paths(self,
paths: typing.List[typing.List[str]]) -> None:
self._defxs = self._defxs[:len(paths)]
for [index, [source_name, path]] in enumerate(paths):
if index >= len(self._defxs):
self._defxs.append(
Defx(self._vim, self._context, source_name, path, index))
else:
defx = self._defxs[index]
self.cd(defx, defx._source.name, path, self._context.cursor)
self._update_paths(index, path)
def _check_bufnr(self, bufnr: int) -> bool:
return (bool(self._vim.call('bufexists', bufnr)) and
bufnr != self._vim.call('bufnr', '%') and
self._vim.call('getbufvar', bufnr, '&filetype') != 'defx')
def _clear_prop_types(self) -> None:
self._vim.call('defx#util#call_atomic', [
['prop_type_delete', [x]] for x in self._proptypes
])
self._proptypes = set()
def _update_highlights(self, columns_highlights: typing.List[
typing.Tuple[str, int, int, int]]) -> None:
commands = []
if self._has_textprop:
for proptype in self._proptypes:
commands.append(['prop_remove', [{'type': proptype}]])
for highlight in columns_highlights:
if highlight[0] not in self._proptypes:
commands.append(
['prop_type_add',
[highlight[0], {'highlight': highlight[0]}]]
)
self._proptypes.add(highlight[0])
commands.append(
['prop_add',
[highlight[1] + 1, highlight[2] + 1,
{'end_col': highlight[3] + 1, 'type': highlight[0]}]]
)
else:
commands.append(['nvim_buf_clear_namespace',
[self._bufnr, self._ns, 0, -1]])
commands += [['nvim_buf_add_highlight',
[self._bufnr, self._ns, x[0], x[1], x[2], x[3]]]
for x in columns_highlights]
self._vim.call('defx#util#call_atomic', commands)
if self._has_textprop:
# Note: redraw is needed for text props
self._vim.command('redraw')