mirror of
https://github.com/SpaceVim/SpaceVim.git
synced 2025-02-03 17:00:05 +08:00
482 lines
14 KiB
Python
482 lines
14 KiB
Python
# ============================================================================
|
|
# FILE: file.py
|
|
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
|
|
# License: MIT license
|
|
# ============================================================================
|
|
|
|
import copy
|
|
import importlib
|
|
from pathlib import Path
|
|
import shutil
|
|
import time
|
|
import typing
|
|
|
|
from defx.action import ActionAttr
|
|
from defx.action import ActionTable
|
|
from defx.base.kind import Base
|
|
from defx.clipboard import ClipboardAction
|
|
from defx.context import Context
|
|
from defx.defx import Defx
|
|
from defx.util import cd, cwd_input, confirm, error
|
|
from defx.util import readable, Nvim
|
|
from defx.view import View
|
|
|
|
_action_table: typing.Dict[str, ActionTable] = {}
|
|
|
|
ACTION_FUNC = typing.Callable[[View, Defx, Context], None]
|
|
|
|
|
|
def action(name: str, attr: ActionAttr = ActionAttr.NONE
|
|
) -> typing.Callable[[ACTION_FUNC], ACTION_FUNC]:
|
|
def wrapper(func: ACTION_FUNC) -> ACTION_FUNC:
|
|
_action_table[name] = ActionTable(func=func, attr=attr)
|
|
|
|
def inner_wrapper(view: View, defx: Defx, context: Context) -> None:
|
|
return func(view, defx, context)
|
|
return inner_wrapper
|
|
return wrapper
|
|
|
|
|
|
class Kind(Base):
|
|
|
|
def __init__(self, vim: Nvim) -> None:
|
|
self.vim = vim
|
|
self.name = 'file'
|
|
|
|
def get_actions(self) -> typing.Dict[str, ActionTable]:
|
|
actions = copy.copy(super().get_actions())
|
|
actions.update(_action_table)
|
|
return actions
|
|
|
|
|
|
def check_overwrite(view: View, dest: Path, src: Path) -> Path:
|
|
s_stat = src.stat()
|
|
s_mtime = s_stat.st_mtime
|
|
view.print_msg(f' src: {src} {s_stat.st_size} bytes')
|
|
view.print_msg(f' {time.strftime("%c", time.localtime(s_mtime))}')
|
|
d_stat = dest.stat()
|
|
d_mtime = d_stat.st_mtime
|
|
view.print_msg(f'dest: {dest} {d_stat.st_size} bytes')
|
|
view.print_msg(f' {time.strftime("%c", time.localtime(d_mtime))}')
|
|
|
|
choice: int = view._vim.call('defx#util#confirm',
|
|
f'{dest} already exists. Overwrite?',
|
|
'&Force\n&No\n&Rename\n&Time\n&Underbar', 0)
|
|
ret: Path = Path('')
|
|
if choice == 1:
|
|
ret = dest
|
|
elif choice == 2:
|
|
ret = Path('')
|
|
elif choice == 3:
|
|
ret = Path(view._vim.call('input', f'{src} -> ', str(dest),
|
|
('dir' if src.is_dir() else 'file')))
|
|
elif choice == 4 and d_mtime < s_mtime:
|
|
ret = src
|
|
elif choice == 5:
|
|
ret = Path(str(dest) + '_')
|
|
return ret
|
|
|
|
|
|
@action(name='cd')
|
|
def _cd(view: View, defx: Defx, context: Context) -> None:
|
|
"""
|
|
Change the current directory.
|
|
"""
|
|
path = Path(context.args[0]) if context.args else Path.home()
|
|
path = Path(defx._cwd).joinpath(path).resolve()
|
|
if not readable(path) or not path.is_dir():
|
|
error(view._vim, f'{path} is not readable directory')
|
|
return
|
|
|
|
prev_cwd = defx._cwd
|
|
view.cd(defx, str(path), context.cursor)
|
|
if context.args and context.args[0] == '..':
|
|
view.search_file(Path(prev_cwd), defx._index)
|
|
|
|
|
|
@action(name='change_vim_cwd', attr=ActionAttr.NO_TAGETS)
|
|
def _change_vim_cwd(view: View, defx: Defx, context: Context) -> None:
|
|
"""
|
|
Change the current working directory.
|
|
"""
|
|
cd(view._vim, defx._cwd)
|
|
|
|
|
|
@action(name='check_redraw', attr=ActionAttr.NO_TAGETS)
|
|
def _check_redraw(view: View, defx: Defx, context: Context) -> None:
|
|
root = defx.get_root_candidate()['action__path']
|
|
if not root.exists():
|
|
return
|
|
mtime = root.stat().st_mtime
|
|
if mtime != defx._mtime:
|
|
view.redraw(True)
|
|
|
|
|
|
@action(name='copy')
|
|
def _copy(view: View, defx: Defx, context: Context) -> None:
|
|
if not context.targets:
|
|
return
|
|
|
|
message = 'Copy to the clipboard: {}'.format(
|
|
str(context.targets[0]['action__path'])
|
|
if len(context.targets) == 1
|
|
else str(len(context.targets)) + ' files')
|
|
view.print_msg(message)
|
|
|
|
view._clipboard.action = ClipboardAction.COPY
|
|
view._clipboard.candidates = context.targets
|
|
|
|
|
|
@action(name='drop')
|
|
def _drop(view: View, defx: Defx, context: Context) -> None:
|
|
"""
|
|
Open like :drop.
|
|
"""
|
|
cwd = view._vim.call('getcwd')
|
|
command = context.args[0] if context.args else 'edit'
|
|
|
|
for target in context.targets:
|
|
path = target['action__path']
|
|
|
|
if path.is_dir():
|
|
view.cd(defx, str(path), context.cursor)
|
|
continue
|
|
|
|
bufnr = view._vim.call('bufnr', f'^{path}$')
|
|
winids = view._vim.call('win_findbuf', bufnr)
|
|
|
|
if winids:
|
|
view._vim.call('win_gotoid', winids[0])
|
|
else:
|
|
if context.prev_winid != view._winid and view._vim.call(
|
|
'win_id2win', context.prev_winid):
|
|
view._vim.call('win_gotoid', context.prev_winid)
|
|
else:
|
|
view._vim.command('wincmd w')
|
|
try:
|
|
path = path.relative_to(cwd)
|
|
except ValueError:
|
|
pass
|
|
view._vim.call('defx#util#execute_path', command, str(path))
|
|
|
|
|
|
@action(name='execute_command', attr=ActionAttr.NO_TAGETS)
|
|
def _execute_command(view: View, defx: Defx, context: Context) -> None:
|
|
"""
|
|
Execute the command.
|
|
"""
|
|
save_cwd = view._vim.call('getcwd')
|
|
cd(view._vim, defx._cwd)
|
|
|
|
command = context.args[0] if context.args else view._vim.call(
|
|
'input', 'Command: ', '', 'shellcmd')
|
|
|
|
output = view._vim.call('system', command)
|
|
if output:
|
|
view.print_msg(output)
|
|
|
|
cd(view._vim, save_cwd)
|
|
|
|
|
|
@action(name='execute_system')
|
|
def _execute_system(view: View, defx: Defx, context: Context) -> None:
|
|
"""
|
|
Execute the file by system associated command.
|
|
"""
|
|
for target in context.targets:
|
|
view._vim.call('defx#util#open', str(target['action__path']))
|
|
|
|
|
|
@action(name='move')
|
|
def _move(view: View, defx: Defx, context: Context) -> None:
|
|
if not context.targets:
|
|
return
|
|
|
|
message = 'Move to the clipboard: {}'.format(
|
|
str(context.targets[0]['action__path'])
|
|
if len(context.targets) == 1
|
|
else str(len(context.targets)) + ' files')
|
|
view.print_msg(message)
|
|
|
|
view._clipboard.action = ClipboardAction.MOVE
|
|
view._clipboard.candidates = context.targets
|
|
|
|
|
|
@action(name='new_directory')
|
|
def _new_directory(view: View, defx: Defx, context: Context) -> None:
|
|
"""
|
|
Create a new directory.
|
|
"""
|
|
candidate = view.get_cursor_candidate(context.cursor)
|
|
if not candidate:
|
|
return
|
|
|
|
if candidate['is_opened_tree'] or candidate['is_root']:
|
|
cwd = str(candidate['action__path'])
|
|
else:
|
|
cwd = str(Path(candidate['action__path']).parent)
|
|
|
|
new_filename = cwd_input(
|
|
view._vim, cwd,
|
|
'Please input a new directory name: ', '', 'file')
|
|
if not new_filename:
|
|
return
|
|
filename = Path(cwd).joinpath(new_filename)
|
|
|
|
if not filename:
|
|
return
|
|
if filename.exists():
|
|
error(view._vim, f'{filename} already exists')
|
|
return
|
|
|
|
filename.mkdir(parents=True)
|
|
view.redraw(True)
|
|
view.search_file(filename, defx._index)
|
|
|
|
|
|
@action(name='new_file')
|
|
def _new_file(view: View, defx: Defx, context: Context) -> None:
|
|
"""
|
|
Create a new file and it's parent directories.
|
|
"""
|
|
candidate = view.get_cursor_candidate(context.cursor)
|
|
if not candidate:
|
|
return
|
|
|
|
if candidate['is_opened_tree'] or candidate['is_root']:
|
|
cwd = str(candidate['action__path'])
|
|
else:
|
|
cwd = str(Path(candidate['action__path']).parent)
|
|
|
|
new_filename = cwd_input(
|
|
view._vim, cwd,
|
|
'Please input a new filename: ', '', 'file')
|
|
if not new_filename:
|
|
return
|
|
isdir = new_filename[-1] == '/'
|
|
filename = Path(cwd).joinpath(new_filename)
|
|
|
|
if not filename:
|
|
return
|
|
if filename.exists():
|
|
error(view._vim, f'{filename} already exists')
|
|
return
|
|
|
|
if isdir:
|
|
filename.mkdir(parents=True)
|
|
else:
|
|
filename.parent.mkdir(parents=True, exist_ok=True)
|
|
filename.touch()
|
|
|
|
view.redraw(True)
|
|
view.search_file(filename, defx._index)
|
|
|
|
|
|
@action(name='new_multiple_files')
|
|
def _new_multiple_files(view: View, defx: Defx, context: Context) -> None:
|
|
"""
|
|
Create multiple files.
|
|
"""
|
|
candidate = view.get_cursor_candidate(context.cursor)
|
|
if not candidate:
|
|
return
|
|
|
|
if candidate['is_opened_tree'] or candidate['is_root']:
|
|
cwd = str(candidate['action__path'])
|
|
else:
|
|
cwd = str(Path(candidate['action__path']).parent)
|
|
|
|
save_cwd = view._vim.call('getcwd')
|
|
cd(view._vim, cwd)
|
|
|
|
str_filenames: str = view._vim.call(
|
|
'input', 'Please input new filenames: ', '', 'file')
|
|
cd(view._vim, save_cwd)
|
|
|
|
if not str_filenames:
|
|
return None
|
|
|
|
for name in str_filenames.split():
|
|
is_dir = name[-1] == '/'
|
|
|
|
filename = Path(cwd).joinpath(name)
|
|
if filename.exists():
|
|
error(view._vim, f'{filename} already exists')
|
|
continue
|
|
|
|
if is_dir:
|
|
filename.mkdir(parents=True)
|
|
else:
|
|
if not filename.parent.exists():
|
|
filename.parent.mkdir(parents=True)
|
|
filename.touch()
|
|
|
|
view.redraw(True)
|
|
view.search_file(filename, defx._index)
|
|
|
|
|
|
@action(name='open')
|
|
def _open(view: View, defx: Defx, context: Context) -> None:
|
|
"""
|
|
Open the file.
|
|
"""
|
|
cwd = view._vim.call('getcwd')
|
|
command = context.args[0] if context.args else 'edit'
|
|
for target in context.targets:
|
|
path = target['action__path']
|
|
|
|
if path.is_dir():
|
|
view.cd(defx, str(path), context.cursor)
|
|
continue
|
|
|
|
try:
|
|
path = path.relative_to(cwd)
|
|
except ValueError:
|
|
pass
|
|
view._vim.call('defx#util#execute_path', command, str(path))
|
|
|
|
|
|
@action(name='open_directory')
|
|
def _open_directory(view: View, defx: Defx, context: Context) -> None:
|
|
"""
|
|
Open the directory.
|
|
"""
|
|
if context.args:
|
|
path = Path(context.args[0])
|
|
else:
|
|
for target in context.targets:
|
|
path = target['action__path']
|
|
|
|
if path.is_dir():
|
|
view.cd(defx, str(path), context.cursor)
|
|
|
|
|
|
@action(name='paste', attr=ActionAttr.NO_TAGETS)
|
|
def _paste(view: View, defx: Defx, context: Context) -> None:
|
|
candidate = view.get_cursor_candidate(context.cursor)
|
|
if not candidate:
|
|
return
|
|
|
|
if candidate['is_opened_tree'] or candidate['is_root']:
|
|
cwd = str(candidate['action__path'])
|
|
else:
|
|
cwd = str(Path(candidate['action__path']).parent)
|
|
|
|
action = view._clipboard.action
|
|
dest = None
|
|
for index, candidate in enumerate(view._clipboard.candidates):
|
|
path = candidate['action__path']
|
|
dest = Path(cwd).joinpath(path.name)
|
|
if dest.exists():
|
|
overwrite = check_overwrite(view, dest, path)
|
|
if overwrite == Path(''):
|
|
continue
|
|
dest = overwrite
|
|
|
|
if path == dest:
|
|
continue
|
|
|
|
view.print_msg(
|
|
f'[{index + 1}/{len(view._clipboard.candidates)}] {path}')
|
|
if action == ClipboardAction.COPY:
|
|
if path.is_dir():
|
|
shutil.copytree(str(path), dest)
|
|
else:
|
|
shutil.copy2(str(path), dest)
|
|
elif action == ClipboardAction.MOVE:
|
|
shutil.move(str(path), cwd)
|
|
view._vim.command('redraw')
|
|
view._vim.command('echo')
|
|
|
|
view.redraw(True)
|
|
if dest:
|
|
view.search_file(dest, defx._index)
|
|
|
|
|
|
@action(name='remove', attr=ActionAttr.REDRAW)
|
|
def _remove(view: View, defx: Defx, context: Context) -> None:
|
|
"""
|
|
Delete the file or directory.
|
|
"""
|
|
if not context.targets:
|
|
return
|
|
|
|
force = context.args[0] == 'force' if context.args else False
|
|
if not force:
|
|
message = 'Are you sure you want to delete {}?'.format(
|
|
str(context.targets[0]['action__path'])
|
|
if len(context.targets) == 1
|
|
else str(len(context.targets)) + ' files')
|
|
if not confirm(view._vim, message):
|
|
return
|
|
|
|
for target in context.targets:
|
|
path = target['action__path']
|
|
|
|
if path.is_dir():
|
|
shutil.rmtree(str(path))
|
|
else:
|
|
path.unlink()
|
|
|
|
|
|
@action(name='remove_trash')
|
|
def _remove_trash(view: View, defx: Defx, context: Context) -> None:
|
|
"""
|
|
Delete the file or directory.
|
|
"""
|
|
if not context.targets:
|
|
return
|
|
|
|
if not importlib.util.find_spec('send2trash'):
|
|
error(view._vim, '"Send2Trash" is not installed')
|
|
return
|
|
|
|
force = context.args[0] == 'force' if context.args else False
|
|
if not force:
|
|
message = 'Are you sure you want to delete {}?'.format(
|
|
str(context.targets[0]['action__path'])
|
|
if len(context.targets) == 1
|
|
else str(len(context.targets)) + ' files')
|
|
if not confirm(view._vim, message):
|
|
return
|
|
|
|
import send2trash
|
|
for target in context.targets:
|
|
send2trash.send2trash(str(target['action__path']))
|
|
view.redraw(True)
|
|
|
|
|
|
@action(name='rename')
|
|
def _rename(view: View, defx: Defx, context: Context) -> None:
|
|
"""
|
|
Rename the file or directory.
|
|
"""
|
|
|
|
if len(context.targets) > 1:
|
|
# ex rename
|
|
view._vim.call('defx#exrename#create_buffer',
|
|
[{'action__path': str(x['action__path'])}
|
|
for x in context.targets],
|
|
{'buffer_name': 'defx'})
|
|
return
|
|
|
|
for target in context.targets:
|
|
old = target['action__path']
|
|
new_filename = cwd_input(
|
|
view._vim, defx._cwd,
|
|
f'Old name: {old}\nNew name: ', str(old), 'file')
|
|
view._vim.command('redraw')
|
|
if not new_filename:
|
|
return
|
|
new = Path(defx._cwd).joinpath(new_filename)
|
|
if not new or new == old:
|
|
continue
|
|
if str(new).lower() != str(old).lower() and new.exists():
|
|
error(view._vim, f'{new} already exists')
|
|
continue
|
|
|
|
old.rename(new)
|
|
|
|
view.redraw(True)
|
|
view.search_file(new, defx._index)
|