1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-02-02 22:50:06 +08:00

Update defx.nvim (#3803)

This commit is contained in:
Wang Shidong 2020-10-31 15:58:52 +08:00 committed by GitHub
parent 15a511c32f
commit af773b6bff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1099 additions and 364 deletions

View File

@ -1,5 +1,8 @@
## Forked repos ## Forked repos
- [defx.nvim](https://github.com/Shougo/defx.nvim/commit/df5e6ea6734dc002919ea41786668069fa0b497d)
### checkers layer ### checkers layer
- neomake - neomake

View File

@ -8,7 +8,7 @@
## Environment Information ## Environment Information
* defx version(SHA1): * plugin version(SHA1):
* OS: * OS:
@ -20,7 +20,7 @@
```vim ```vim
" Your minimal init.vim/vimrc " Your minimal init.vim/vimrc
set runtimepath+=~/path/to/defx.nvim/ set runtimepath+=~/path/to/plugin/
``` ```

View File

@ -0,0 +1,17 @@
name: Lint and test
on: [push, pull_request]
jobs:
lint:
name: Lint and test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install
run: |
sudo apt install python3-setuptools python3-wheel
- name: Lint
run: make --keep-going install-user test lint

View File

@ -1,14 +0,0 @@
dist: xenial
language: python
python:
- 3.6
- 3.7
install:
- eval "$(curl -Ss https://raw.githubusercontent.com/neovim/bot-ci/master/scripts/travis-setup.sh) nightly-x64"
- make install
script:
- make --keep-going test lint

View File

@ -1,14 +1,10 @@
PATH := ./vim-themis/bin:$(PATH) PATH := $(HOME)/.local/bin:$(PATH)
export THEMIS_VIM := nvim
export THEMIS_ARGS := -e -s --headless
export THEMIS_HOME := ./vim-themis
install:
pip3 install --upgrade -r test/requirements.txt
install: vim-themis install-user:
pip install --upgrade -r test/requirements.txt pip3 install --user --upgrade -r test/requirements.txt
install-user: vim-themis
pip install --user --upgrade -r test/requirements.txt
lint: lint:
vint --version vint --version
@ -20,12 +16,7 @@ lint:
mypy --ignore-missing-imports --follow-imports=skip --strict rplugin/python3/defx mypy --ignore-missing-imports --follow-imports=skip --strict rplugin/python3/defx
test: test:
# themis --version
# themis test/autoload/*
pytest --version pytest --version
pytest pytest
vim-themis:
git clone https://github.com/thinca/vim-themis vim-themis
.PHONY: install lint test .PHONY: install lint test

View File

@ -37,7 +37,7 @@ It replaces the deprecated vimfiler plugin.
## Installation ## Installation
**Note:** defx requires Neovim 0.3.0+ or Vim8.1+ with Python3.6.1+. See **Note:** defx requires Neovim 0.4.0+ or Vim8.2+ with Python3.6.1+. See
[requirements](#requirements) if you aren't sure whether you have this. [requirements](#requirements) if you aren't sure whether you have this.
For vim-plug For vim-plug
@ -70,10 +70,12 @@ For manual installation(not recommended)
## Requirements ## Requirements
defx requires Python3.6.1+ and Neovim(0.3.0+) or Vim8.1+ with if\_python3. If defx requires Python3.6.1+ and Neovim(0.4.0+) or Vim8.2+ with if\_python3. If
`:echo has("python3")` returns `1`, then you have python 3 support; otherwise, `:echo has("python3")` returns `1`, then you have python 3 support; otherwise,
see below. see below.
Note: The latest Neovim is recommended, because it is faster.
You can enable Python3 interface with pip: You can enable Python3 interface with pip:
pip3 install --user pynvim pip3 install --user pynvim
@ -92,26 +94,25 @@ If Defx was installed prior to Python support being added to Neovim,
## Configuration Examples ## Configuration Examples
```vim Please see `:help defx-examples`.
" Todo
```
## Screenshots ## Screenshots
![multi root feature](https://user-images.githubusercontent.com/41495/45696476-ac9d0a80-bb9e-11e8-9ee2-120ac7d0f045.png) ![Multi root feature](https://user-images.githubusercontent.com/41495/45696476-ac9d0a80-bb9e-11e8-9ee2-120ac7d0f045.png)
![Defx -split=vertical](https://user-images.githubusercontent.com/2835826/45823772-7190f900-bcbc-11e8-9727-3dda3ce4c07c.png) ![Defx -split=vertical](https://user-images.githubusercontent.com/2835826/45823772-7190f900-bcbc-11e8-9727-3dda3ce4c07c.png)
![Defx -new](https://user-images.githubusercontent.com/3047695/45927914-7f07e680-bf3b-11e8-9b36-755e1eec2a8f.png) ![Defx -new](https://user-images.githubusercontent.com/3047695/45927914-7f07e680-bf3b-11e8-9b36-755e1eec2a8f.png)
![Defx + neovim-qt](https://user-images.githubusercontent.com/1314340/48659914-0b4a0c00-ea9c-11e8-9953-2f2d5ca7f24a.png) ![Defx + neovim-qt](https://user-images.githubusercontent.com/1314340/48659914-0b4a0c00-ea9c-11e8-9953-2f2d5ca7f24a.png)
![custom icon](https://user-images.githubusercontent.com/10108377/59982828-ac93d480-9620-11e9-8c10-51909cfeaf94.png) ![Custom icon](https://user-images.githubusercontent.com/10108377/59982828-ac93d480-9620-11e9-8c10-51909cfeaf94.png)
![custom icon2](https://user-images.githubusercontent.com/3021667/55260000-95ba2d80-523d-11e9-877c-756a080a9a28.png) ![Custom icon2](https://user-images.githubusercontent.com/3021667/55260000-95ba2d80-523d-11e9-877c-756a080a9a28.png)
![custom icon3](https://user-images.githubusercontent.com/10397021/57774111-3f04a680-774c-11e9-852a-53c394f672ef.png) ![Custom icon3](https://user-images.githubusercontent.com/10397021/57774111-3f04a680-774c-11e9-852a-53c394f672ef.png)
![custom icon4](https://user-images.githubusercontent.com/12205650/58801907-d9346d80-85d9-11e9-8a2d-de4635aa1eba.png) ![Custom icon4](https://user-images.githubusercontent.com/12205650/58801907-d9346d80-85d9-11e9-8a2d-de4635aa1eba.png)
![custom icon5](https://user-images.githubusercontent.com/11615211/82411894-381e1b80-9aa5-11ea-9552-fd9847fe25e3.png) ![Custom icon5](https://user-images.githubusercontent.com/11615211/82411894-381e1b80-9aa5-11ea-9552-fd9847fe25e3.png)
![Defx on kitty](https://user-images.githubusercontent.com/8403993/51080184-d29e6b80-16b5-11e9-802b-7c2f56705e2e.png) ![Defx on kitty](https://user-images.githubusercontent.com/8403993/51080184-d29e6b80-16b5-11e9-802b-7c2f56705e2e.png)
![Defx in SpaceVim](https://user-images.githubusercontent.com/13142418/54086225-85233f80-4382-11e9-8091-7f387319b90a.png) ![Defx in SpaceVim](https://user-images.githubusercontent.com/13142418/54086225-85233f80-4382-11e9-8091-7f387319b90a.png)
![Variable column](https://user-images.githubusercontent.com/19503791/56090130-58f26580-5ed0-11e9-8b66-e684cb11b0d1.png) ![Variable column](https://user-images.githubusercontent.com/19503791/56090130-58f26580-5ed0-11e9-8b66-e684cb11b0d1.png)
![Denite action call](https://user-images.githubusercontent.com/41671631/56280845-a6bfd580-613d-11e9-857a-d81f2633eeab.png) ![Denite action call](https://user-images.githubusercontent.com/41671631/56280845-a6bfd580-613d-11e9-857a-d81f2633eeab.png)
![Defx floating window](https://user-images.githubusercontent.com/24732170/59892964-1c823f00-9416-11e9-8369-2e21910e168c.png) ![Defx floating window](https://user-images.githubusercontent.com/24732170/59892964-1c823f00-9416-11e9-8369-2e21910e168c.png)
![Horizon colorscheme](https://user-images.githubusercontent.com/324519/63241202-a4fb4100-c207-11e9-9060-c3c04608ea7b.png) ![Horizon colorscheme](https://user-images.githubusercontent.com/324519/63241202-a4fb4100-c207-11e9-9060-c3c04608ea7b.png)
![Image preview](https://user-images.githubusercontent.com/41671631/85951370-5d9c2000-b995-11ea-8a3d-2c304d21cc4c.gif)
![Defx + vim-quickui](https://user-images.githubusercontent.com/32936898/92196371-bd390f00-eea1-11ea-957e-5dcde77afd3e.png)

View File

@ -9,11 +9,32 @@ function! defx#initialize() abort
endfunction endfunction
function! defx#start(paths, user_context) abort function! defx#start(paths, user_context) abort
let prev_winid = win_getid()
call defx#initialize() call defx#initialize()
let context = defx#init#_context(a:user_context) let context = defx#init#_context(a:user_context)
let paths = a:paths let paths = map(a:paths, "[v:val[0], fnamemodify(v:val[1], ':p')]")
let paths = map(paths, "fnamemodify(v:val, ':p')")
call defx#util#rpcrequest('_defx_start', [paths, context], v:false) call defx#util#rpcrequest('_defx_start',
\ [paths, context], v:false)
if context['search'] !=# ''
call defx#call_action('search', [context['search']])
endif
if !context['focus']
" Restore the window
call win_gotoid(prev_winid)
endif
endfunction
function! defx#start_candidates(candidates, user_context) abort
call defx#initialize()
let context = defx#init#_context(a:user_context)
let listfile = tempname()
call writefile(a:candidates, listfile)
let paths = [['file/list', listfile]]
call defx#util#rpcrequest('_defx_start',
\ [paths, context], v:false)
if context['search'] !=# '' if context['search'] !=# ''
call defx#call_action('search', [context['search']]) call defx#call_action('search', [context['search']])
endif endif

View File

@ -54,7 +54,7 @@ function! defx#exrename#create_buffer(candidates, ...) abort
silent! syntax clear defxExrenameOriginal silent! syntax clear defxExrenameOriginal
" validate candidates and register " validate candidates and register
let unique_filenames = [] let unique_filenames = {}
let b:exrename.candidates = [] let b:exrename.candidates = []
let b:exrename.filenames = [] let b:exrename.filenames = []
let cnt = 1 let cnt = 1
@ -67,13 +67,15 @@ function! defx#exrename#create_buffer(candidates, ...) abort
if !filewritable(candidate.action__path) if !filewritable(candidate.action__path)
\ && !isdirectory(candidate.action__path) \ && !isdirectory(candidate.action__path)
redraw redraw
echo candidate.action__path 'does not exist. Skip.' call defx#util#print_error(
\ candidate.action__path . ' does not exist. Skip.')
continue continue
endif endif
" make sure that the 'action__path' is unique " make sure that the 'action__path' is unique
if index(unique_filenames, candidate.action__path) != -1 if has_key(unique_filenames, candidate.action__path)
redraw redraw
echo candidate.action__path 'is duplicated. Skip.' call defx#util#print_error(
\ candidate.action__path . ' is duplicated. Skip.')
continue continue
endif endif
" create filename " create filename
@ -90,11 +92,14 @@ function! defx#exrename#create_buffer(candidates, ...) abort
\ '/'.printf('^\%%%dl%s$', cnt, \ '/'.printf('^\%%%dl%s$', cnt,
\ escape(s:escape_pattern(filename), '/')).'/' \ escape(s:escape_pattern(filename), '/')).'/'
" register " register
call add(unique_filenames, candidate.action__path) let unique_filenames[candidate.action__path] = 1
call add(b:exrename.candidates, candidate) call add(b:exrename.candidates, candidate)
call add(b:exrename.filenames, filename) call add(b:exrename.filenames, filename)
let cnt += 1 let cnt += 1
endfor endfor
let b:exrename.unique_filenames = unique_filenames
" write filenames " write filenames
let [undolevels, &undolevels] = [&undolevels, -1] let [undolevels, &undolevels] = [&undolevels, -1]
try try
@ -115,7 +120,7 @@ endfunction
function! s:do_rename() abort function! s:do_rename() abort
if line('$') != len(b:exrename.filenames) if line('$') != len(b:exrename.filenames)
echohl Error | echo 'Invalid rename buffer!' | echohl None call defx#util#print_error('Invalid rename buffer!')
return return
endif endif
@ -129,22 +134,44 @@ function! s:do_rename() abort
echo printf('(%'.len(max).'d/%d): %s -> %s', echo printf('(%'.len(max).'d/%d): %s -> %s',
\ linenr, max, filename, getline(linenr)) \ linenr, max, filename, getline(linenr))
if filename !=# getline(linenr) if filename ==# getline(linenr)
let old_file = b:exrename.candidates[linenr - 1].action__path let linenr += 1
let new_file = expand(getline(linenr)) continue
if new_file !~# '^\%(\a\a\+:\)\|^\%(\a:\|/\)'
let new_file = b:exrename.cwd . new_file
endif
if rename(old_file, new_file)
" Rename error
continue
endif
" update b:exrename
let b:exrename.filenames[linenr - 1] = getline(linenr)
let b:exrename.candidates[linenr - 1].action__path = new_file
endif endif
let old_file = b:exrename.candidates[linenr - 1].action__path
let new_file = expand(getline(linenr))
if !s:is_absolute(new_file)
" Convert to absolute path
let new_file = b:exrename.cwd . new_file
endif
if filereadable(new_file) || isdirectory(new_file)
" new_file is already exists.
redraw
call defx#util#print_error(
\ new_file . ' is already exists. Skip.')
let linenr += 1
continue
endif
if rename(old_file, new_file)
" Rename error
redraw
call defx#util#print_error(
\ new_file . ' is rename error. Skip.')
let linenr += 1
continue
endif
call defx#util#buffer_rename(bufnr(old_file), new_file)
" update b:exrename
let b:exrename.filenames[linenr - 1] = getline(linenr)
let b:exrename.candidates[linenr - 1].action__path = new_file
let linenr += 1 let linenr += 1
endwhile endwhile
@ -178,7 +205,7 @@ function! s:check_lines() abort
endif endif
if line('$') != len(b:exrename.filenames) if line('$') != len(b:exrename.filenames)
echohl Error | echo 'Invalid rename buffer!' | echohl None call defx#util#print_error('Invalid rename buffer!')
return return
endif endif
endfunction endfunction

View File

@ -16,6 +16,7 @@ function! defx#init#_initialize() abort
augroup END augroup END
let g:defx#_histories = [] let g:defx#_histories = []
let g:defx#_previewed_buffers = {}
endfunction endfunction
function! defx#init#_channel() abort function! defx#init#_channel() abort
if !has('python3') if !has('python3')
@ -23,12 +24,12 @@ function! defx#init#_channel() abort
\ 'defx requires Python3 support("+python3").') \ 'defx requires Python3 support("+python3").')
return v:true return v:true
endif endif
if has('nvim') && !has('nvim-0.3.0') if has('nvim') && !has('nvim-0.4.0')
call defx#util#print_error('defx requires nvim 0.3.0+.') call defx#util#print_error('defx requires nvim 0.4.0+.')
return v:true return v:true
endif endif
if !has('nvim') && v:version < 801 if !has('nvim') && !defx#util#has_textprop()
call defx#util#print_error('defx requires Vim 8.1+.') call defx#util#print_error('defx requires Vim 8.2+ with textprop.')
return v:true return v:true
endif endif
@ -95,25 +96,32 @@ function! defx#init#_user_options() abort
\ 'auto_cd': v:false, \ 'auto_cd': v:false,
\ 'auto_recursive_level': 0, \ 'auto_recursive_level': 0,
\ 'buffer_name': 'default', \ 'buffer_name': 'default',
\ 'close': v:false,
\ 'columns': 'mark:indent:icon:filename:type', \ 'columns': 'mark:indent:icon:filename:type',
\ 'direction': '', \ 'direction': '',
\ 'filtered_files': '',
\ 'floating_preview': v:false,
\ 'focus': v:true,
\ 'ignored_files': '.*', \ 'ignored_files': '.*',
\ 'listed': v:false, \ 'listed': v:false,
\ 'new': v:false, \ 'new': v:false,
\ 'preview_height': &previewheight,
\ 'preview_width': 40,
\ 'profile': v:false, \ 'profile': v:false,
\ 'resume': v:false, \ 'resume': v:false,
\ 'root_marker': '[in]: ', \ 'root_marker': '[in] ',
\ 'search': '', \ 'search': '',
\ 'session_file': '', \ 'session_file': '',
\ 'show_ignored_files': v:false, \ 'show_ignored_files': v:false,
\ 'split': 'no',
\ 'sort': 'filename', \ 'sort': 'filename',
\ 'split': 'no',
\ 'toggle': v:false, \ 'toggle': v:false,
\ 'wincol': &columns / 4, \ 'wincol': &columns / 4,
\ 'winheight': 30, \ 'winheight': 30,
\ 'winrelative': 'editor', \ 'winrelative': 'editor',
\ 'winrow': &lines / 3, \ 'winrow': &lines / 3,
\ 'winwidth': 90, \ 'winwidth': 90,
\ 'vertical_preview': v:false,
\ } \ }
endfunction endfunction
function! s:internal_options() abort function! s:internal_options() abort

View File

@ -40,6 +40,9 @@ endfunction
function! defx#util#has_yarp() abort function! defx#util#has_yarp() abort
return !has('nvim') || get(g:, 'defx#enable_yarp', 0) return !has('nvim') || get(g:, 'defx#enable_yarp', 0)
endfunction endfunction
function! defx#util#has_textprop() abort
return v:version >= 802 && exists('*prop_add')
endfunction
function! defx#util#execute_path(command, path) abort function! defx#util#execute_path(command, path) abort
try try
@ -127,8 +130,16 @@ function! s:parse_options(cmdline) abort
\ s:eval_cmdline(a:cmdline) : a:cmdline \ s:eval_cmdline(a:cmdline) : a:cmdline
for s in split(cmdline, s:re_unquoted_match('\%(\\\@<!\s\)\+')) for s in split(cmdline, s:re_unquoted_match('\%(\\\@<!\s\)\+'))
let arg = substitute(s, '\\\( \)', '\1', 'g') let s = substitute(s, '\\\( \)', '\1', 'g')
let arg_key = substitute(arg, '=\zs.*$', '', '') let splits = split(s, '\a\a\+\zs:')
if len(splits) == 1
let source_name = 'file'
let source_arg = s
else
let source_name = splits[0]
let source_arg = join(splits[1:], ':')
endif
let arg_key = substitute(s, '=\zs.*$', '', '')
let name = substitute(tr(arg_key, '-', '_'), '=$', '', '')[1:] let name = substitute(tr(arg_key, '-', '_'), '=$', '', '')[1:]
if name =~# '^no_' if name =~# '^no_'
@ -136,13 +147,13 @@ function! s:parse_options(cmdline) abort
let value = v:false let value = v:false
else else
let value = (arg_key =~# '=$') ? let value = (arg_key =~# '=$') ?
\ s:remove_quote_pairs(arg[len(arg_key) :]) : v:true \ s:remove_quote_pairs(s[len(arg_key) :]) : v:true
endif endif
if index(keys(defx#init#_user_options()), name) >= 0 if index(keys(defx#init#_user_options()), name) >= 0
let options[name] = value let options[name] = value
else else
call add(args, arg) call add(args, [source_name, source_arg])
endif endif
endfor endfor
@ -323,3 +334,134 @@ function! s:strwidthpart_reverse(str, width) abort
let vcol = strwidth(str) - a:width let vcol = strwidth(str) - a:width
return matchstr(str, '\%>' . (vcol < 0 ? 0 : vcol) . 'v.*') return matchstr(str, '\%>' . (vcol < 0 ? 0 : vcol) . 'v.*')
endfunction endfunction
function! defx#util#buffer_rename(bufnr, new_filename) abort
if a:bufnr < 0 || !bufloaded(a:bufnr)
return
endif
let hidden = &hidden
set hidden
let bufnr_save = bufnr('%')
noautocmd silent! execute 'buffer' a:bufnr
silent execute (&l:buftype ==# '' ? 'saveas!' : 'file')
\ fnameescape(a:new_filename)
if &l:buftype ==# ''
" Remove old buffer.
silent! bdelete! #
endif
noautocmd silent execute 'buffer' bufnr_save
let &hidden = hidden
endfunction
function! defx#util#buffer_delete(bufnr) abort
if a:bufnr < 0
return
endif
let winid = get(win_findbuf(a:bufnr), 0, -1)
if winid > 0
let winid_save = win_getid()
call win_gotoid(winid)
noautocmd silent enew
execute 'silent! bdelete!' a:bufnr
call win_gotoid(winid_save)
else
execute 'silent! bdelete!' a:bufnr
endif
endfunction
function! defx#util#_get_preview_window() abort
" Note: For popup preview feature
if exists('*popup_findpreview') && popup_findpreview() > 0
return 1
endif
return len(filter(range(1, winnr('$')),
\ "getwinvar(v:val, '&previewwindow') ==# 1"))
endfunction
function! defx#util#preview_file(context, filename) abort
let preview_width = str2nr(a:context.preview_width)
let preview_height = str2nr(a:context.preview_height)
let pos = win_screenpos(win_getid())
let win_width = winwidth(0)
let win_height = winheight(0)
if a:context.vertical_preview
call defx#util#execute_path(
\ 'silent rightbelow vertical pedit!', a:filename)
wincmd P
if a:context.floating_preview && exists('*nvim_win_set_config')
if a:context['split'] ==# 'floating'
let win_row = str2nr(a:context['winrow'])
let win_col = str2nr(a:context['wincol'])
else
let win_row = pos[0] - 1
let win_col = pos[1] - 1
endif
let win_col += win_width
if (win_col + preview_width) > &columns
let win_col -= preview_width
endif
call nvim_win_set_config(win_getid(), {
\ 'relative': 'editor',
\ 'row': win_row,
\ 'col': win_col,
\ 'width': preview_width,
\ 'height': preview_height,
\ })
else
execute 'vert resize ' . preview_width
endif
else
call defx#util#execute_path('silent aboveleft pedit!', a:filename)
wincmd P
if a:context.floating_preview && exists('*nvim_win_set_config')
let win_row = pos[0] - 1
let win_col = pos[1] + 1
if win_row <= preview_height
let win_row += win_height + 1
let anchor = 'NW'
else
let anchor = 'SW'
endif
call nvim_win_set_config(0, {
\ 'relative': 'editor',
\ 'anchor': anchor,
\ 'row': win_row,
\ 'col': win_col,
\ 'width': preview_width,
\ 'height': preview_height,
\ })
else
execute 'resize ' . preview_height
endif
endif
if exists('#User#defx-preview')
doautocmd User defx-preview
endif
endfunction
function! defx#util#call_atomic(calls) abort
let results = []
for [name, args] in a:calls
try
call add(results, call(name, args))
catch
call defx#util#print_error(v:exception)
return [results, v:exception]
endtry
endfor
return [results, v:null]
endfunction

View File

@ -1,6 +1,6 @@
*defx.txt* Dark powered file explorer for neovim/Vim8. *defx.txt* Dark powered file explorer for neovim/Vim8.
Version: 1.0 Version: 2.0
Author: Shougo <Shougo.Matsu at gmail.com> Author: Shougo <Shougo.Matsu at gmail.com>
License: MIT license License: MIT license
@ -29,7 +29,9 @@ INTRODUCTION *defx-introduction*
============================================================================== ==============================================================================
INSTALL *defx-install* INSTALL *defx-install*
Note: defx requires Neovim 0.3.0+ or Vim8.1+ with Python3.6.1+. Note: defx requires Neovim 0.4.0+ or Vim8.2+ with Python3.6.1+.
Note: The latest Neovim is recommended, because it is faster.
Please install nvim-yarp plugin for Vim8. Please install nvim-yarp plugin for Vim8.
https://github.com/roxma/nvim-yarp https://github.com/roxma/nvim-yarp
@ -233,6 +235,18 @@ cd *defx-action-cd*
Action args: Action args:
0. new current directory path 0. new current directory path
change_filtered_files *defx-action-change_filtered_files*
Change |defx-option-filtered-files| dynamically.
Action args:
0. filtered files pattern
change_ignored_files *defx-action-change_ignored_files*
Change |defx-option-ignored-files| dynamically.
Action args:
0. ignored files pattern
change_vim_cwd *change_vim_cwd* change_vim_cwd *change_vim_cwd*
Change current working directory to the current directory. Change current working directory to the current directory.
Note: It changes global current directory if the window has Note: It changes global current directory if the window has
@ -343,6 +357,14 @@ paste *defx-action-paste*
Note: It is used after |defx-action-copy| or Note: It is used after |defx-action-copy| or
|defx-action-move|. |defx-action-move|.
preview *defx-action-preview*
Preview the file. Close the preview window if it is already
exists.
Note: "ueberzug" and "bash" commands are needed to preview
image files.
https://pypi.org/project/ueberzug/
Note: The image preview is for X11 only.
print *defx-action-print* print *defx-action-print*
Print the filename. Print the filename.
@ -418,6 +440,9 @@ toggle_select_visual
yank_path *defx-action-yank_path* yank_path *defx-action-yank_path*
Yank the all candidates path. Yank the all candidates path.
Action args:
0. |fnamemodify()| modifier(The default is "")
------------------------------------------------------------------------------ ------------------------------------------------------------------------------
OPTIONS *defx-options* OPTIONS *defx-options*
@ -425,7 +450,7 @@ OPTIONS *defx-options*
-no-{option-name} -no-{option-name}
Disable {option-name} flag. Disable {option-name} flag.
Note: If you use both {option-name} and -no-{option-name} in Note: If you use both {option-name} and -no-{option-name} in
the same denite buffer, it is undefined. the same defx buffer, it is undefined.
*defx-option-auto-cd* *defx-option-auto-cd*
-auto-cd -auto-cd
@ -446,6 +471,11 @@ OPTIONS *defx-options*
Specify defx buffer name. Specify defx buffer name.
Default: "default" Default: "default"
*defx-option-close*
-close
Close defx buffer window.
Default: false
*defx-option-columns* *defx-option-columns*
-columns={columns1:columns2,...} -columns={columns1:columns2,...}
Specify defx columns. Specify defx columns.
@ -458,6 +488,29 @@ OPTIONS *defx-options*
You can use "topleft" or "botright". You can use "topleft" or "botright".
Default: "" Default: ""
*defx-option-focus*
-focus
Focus on the defx buffer after opening a defx buffer.
Default: true
*defx-option-filtered-files*
-filtered-files={pattern}
Specify the filtered files pattern.
The pattern is comma separated.
Default: ""
*defx-option-floating-preview*
-floating-preview
Open the preview window in floating window when
|defx-option-vertical-preview|.
Note: To use it, you need to use neovim
0.4.0+(|nvim_open_win()|).
Note: If you need the feature in Vim8, you should use
'previewpopup' instead.
Default: false
*defx-option-ignored-files* *defx-option-ignored-files*
-ignored-files={pattern} -ignored-files={pattern}
Specify the ignored files pattern. Specify the ignored files pattern.
@ -476,6 +529,19 @@ OPTIONS *defx-options*
Default: false Default: false
*defx-option-preview-height*
-preview-height={preview-height}
Specify the preview window height.
Default: 'previewheight'
*defx-option-preview-width*
-preview-width={preview-width}
Specify the preview width when
|defx-option-vertical-preview|.
Default: 40
*defx-option-profile* *defx-option-profile*
-profile -profile
Enable profile feature. Enable profile feature.
@ -494,7 +560,7 @@ OPTIONS *defx-options*
-root-marker={marker} -root-marker={marker}
Root marker. Root marker.
Default: "[in]: " Default: "[in] "
*defx-option-search* *defx-option-search*
-search={path} -search={path}
@ -542,6 +608,11 @@ OPTIONS *defx-options*
*defx-option-toggle* *defx-option-toggle*
-toggle -toggle
Close defx buffer window if this defx window exists. Close defx buffer window if this defx window exists.
Default: false
*defx-option-vertical-preview*
-vertical-preview
Open the preview window vertically.
Default: false Default: false
*defx-option-wincol* *defx-option-wincol*
@ -704,7 +775,7 @@ EXAMPLES *defx-examples*
nnoremap <silent><buffer><expr> E nnoremap <silent><buffer><expr> E
\ defx#do_action('open', 'vsplit') \ defx#do_action('open', 'vsplit')
nnoremap <silent><buffer><expr> P nnoremap <silent><buffer><expr> P
\ defx#do_action('open', 'pedit') \ defx#do_action('preview')
nnoremap <silent><buffer><expr> o nnoremap <silent><buffer><expr> o
\ defx#do_action('open_tree', 'toggle') \ defx#do_action('open_tree', 'toggle')
nnoremap <silent><buffer><expr> K nnoremap <silent><buffer><expr> K
@ -805,6 +876,12 @@ Q: I want to open file by double click.
A: > A: >
nnoremap <silent><buffer><expr> <2-LeftMouse> defx#do_action('open') nnoremap <silent><buffer><expr> <2-LeftMouse> defx#do_action('open')
Q: I want to separate defx state by tabs.
A: >
Defx -buffer-name=`'defx' . tabpagenr()`
============================================================================== ==============================================================================
COMPATIBILITY *defx-compatibility* COMPATIBILITY *defx-compatibility*

View File

@ -31,6 +31,9 @@ def do_action(view: View, defx: Defx,
""" """
Do "action_name" action. Do "action_name" action.
""" """
if not defx._source:
return True
actions: typing.Dict[str, ActionTable] = defx._source.kind.get_actions() actions: typing.Dict[str, ActionTable] = defx._source.kind.get_actions()
if action_name not in actions: if action_name not in actions:

View File

@ -9,10 +9,12 @@ import typing
from abc import abstractmethod from abc import abstractmethod
from defx.context import Context from defx.context import Context
from defx.util import Nvim from defx.util import Nvim, Candidate
from defx.util import error from defx.util import error
from defx.view import View from defx.view import View
Highlights = typing.List[typing.Tuple[str, int, int]]
class Base: class Base:
@ -20,12 +22,14 @@ class Base:
self.vim: Nvim = vim self.vim: Nvim = vim
self.name: str = 'base' self.name: str = 'base'
self.syntax_name: str = '' self.syntax_name: str = ''
self.highlight_name: str = ''
self.start: int = -1 self.start: int = -1
self.end: int = -1 self.end: int = -1
self.vars: typing.Dict[str, typing.Any] = {} self.vars: typing.Dict[str, typing.Any] = {}
self.is_start_variable: bool = False self.is_start_variable: bool = False
self.is_stop_variable: bool = False self.is_stop_variable: bool = False
self.is_within_variable: bool = False self.is_within_variable: bool = False
self.has_get_with_highlights: bool = False
def on_init(self, view: View, context: Context) -> None: def on_init(self, view: View, context: Context) -> None:
pass pass
@ -33,14 +37,18 @@ class Base:
def on_redraw(self, view: View, context: Context) -> None: def on_redraw(self, view: View, context: Context) -> None:
pass pass
def get(self, context: Context, def get(self, context: Context, candidate: Candidate) -> str:
candidate: typing.Dict[str, typing.Any]) -> str:
return '' return ''
def get_with_variable_text( def get_with_variable_text(
self, context: Context, variable_text: str, self, context: Context, variable_text: str, candidate: Candidate
candidate: typing.Dict[str, typing.Any]) -> str: ) -> typing.Tuple[str, Highlights]:
return '' return ('', [])
def get_with_highlights(
self, context: Context, candidate: Candidate
) -> typing.Tuple[str, Highlights]:
return ('', [])
@abstractmethod @abstractmethod
def length(self, context: Context) -> int: def length(self, context: Context) -> int:

View File

@ -88,6 +88,24 @@ def _call(view: View, defx: Defx, context: Context) -> None:
view._vim.call(function, dict_context) view._vim.call(function, dict_context)
@action(name='change_filtered_files', attr=ActionAttr.REDRAW)
def _change_filtered_files(view: View, defx: Defx, context: Context) -> None:
filtered_files = context.args[0] if context.args else view._vim.call(
'defx#util#input',
f'{".".join(defx._filtered_files)} -> ',
'.'.join(defx._filtered_files))
defx._filtered_files = filtered_files.split(',')
@action(name='change_ignored_files', attr=ActionAttr.REDRAW)
def _change_ignored_files(view: View, defx: Defx, context: Context) -> None:
ignored_files = context.args[0] if context.args else view._vim.call(
'defx#util#input',
f'{".".join(defx._ignored_files)} -> ',
'.'.join(defx._ignored_files))
defx._ignored_files = ignored_files.split(',')
@action(name='clear_select_all', attr=ActionAttr.MARK | ActionAttr.NO_TAGETS) @action(name='clear_select_all', attr=ActionAttr.MARK | ActionAttr.NO_TAGETS)
def _clear_select_all(view: View, defx: Defx, context: Context) -> None: def _clear_select_all(view: View, defx: Defx, context: Context) -> None:
for candidate in [x for x in view._candidates for candidate in [x for x in view._candidates
@ -217,7 +235,7 @@ def _resize(view: View, defx: Defx, context: Context) -> None:
return return
view._context = view._context._replace(winwidth=int(context.args[0])) view._context = view._context._replace(winwidth=int(context.args[0]))
view._resize_window() view._init_window()
view.redraw(True) view.redraw(True)
@ -243,19 +261,7 @@ def _search(view: View, defx: Defx, context: Context) -> None:
return return
search_path = context.args[0] search_path = context.args[0]
path = Path(search_path) view.search_recursive(Path(search_path), defx._index)
parents: typing.List[Path] = []
while view.get_candidate_pos(
path, defx._index) < 0 and path.parent != path:
path = path.parent
parents.append(path)
for parent in reversed(parents):
view.open_tree(parent, defx._index, False, 0)
view.update_candidates()
view.redraw()
view.search_file(Path(search_path), defx._index)
@action(name='toggle_columns', attr=ActionAttr.REDRAW) @action(name='toggle_columns', attr=ActionAttr.REDRAW)
@ -324,7 +330,11 @@ def _toggle_sort(view: View, defx: Defx, context: Context) -> None:
@action(name='yank_path') @action(name='yank_path')
def _yank_path(view: View, defx: Defx, context: Context) -> None: def _yank_path(view: View, defx: Defx, context: Context) -> None:
yank = '\n'.join([str(x['action__path']) for x in context.targets]) mods = context.args[0] if context.args else ''
paths = [str(x['action__path']) for x in context.targets]
if mods:
paths = [view._vim.call('fnamemodify', x, mods) for x in paths]
yank = '\n'.join(paths)
view._vim.call('setreg', '"', yank) view._vim.call('setreg', '"', yank)
if (view._vim.call('has', 'clipboard') or if (view._vim.call('has', 'clipboard') or
view._vim.call('has', 'xterm_clipboard')): view._vim.call('has', 'xterm_clipboard')):

View File

@ -4,9 +4,9 @@
# License: MIT license # License: MIT license
# ============================================================================ # ============================================================================
from defx.base.column import Base from defx.base.column import Base, Highlights
from defx.context import Context from defx.context import Context
from defx.util import Nvim from defx.util import Nvim, Candidate, len_bytes, strwidth
from defx.view import View from defx.view import View
import typing import typing
@ -24,12 +24,12 @@ class Column(Base):
'root_marker_highlight': 'Constant', 'root_marker_highlight': 'Constant',
} }
self.is_stop_variable = True self.is_stop_variable = True
self.has_get_with_highlights = True
self._current_length = 0 self._current_length = 0
self._syntaxes = [ self._syntaxes = [
'directory', 'directory',
'directory_marker', 'directory_marker',
'hidden',
'root', 'root',
'root_marker', 'root_marker',
] ]
@ -41,15 +41,30 @@ class Column(Base):
self._context = context self._context = context
def get_with_variable_text( def get_with_variable_text(
self, context: Context, variable_text: str, self, context: Context, variable_text: str, candidate: Candidate
candidate: typing.Dict[str, typing.Any]) -> str: ) -> typing.Tuple[str, Highlights]:
marker = (self._directory_marker text = variable_text
if candidate['is_directory'] and not candidate['is_root'] highlights = []
else self._file_marker)
return self._truncate(variable_text + marker + candidate['word']) if candidate['is_directory']:
if candidate['is_root']:
root_len = len_bytes(candidate['root_marker'])
highlights = [
(f'{self.highlight_name}_root_marker',
self.start, root_len),
(f'{self.highlight_name}_root',
self.start + root_len,
len_bytes(candidate['word']) - root_len),
]
else:
highlights = [(f'{self.highlight_name}_directory',
self.start, len_bytes(candidate['word']))]
text += candidate['word']
return (self._truncate(text), highlights)
def length(self, context: Context) -> int: def length(self, context: Context) -> int:
max_fnamewidth = max([self._strwidth(x['word']) max_fnamewidth = max([strwidth(self.vim, x['word'])
for x in context.targets]) for x in context.targets])
max_fnamewidth += context.variable_length max_fnamewidth += context.variable_length
max_fnamewidth += len(self._file_marker) max_fnamewidth += len(self._file_marker)
@ -67,63 +82,21 @@ class Column(Base):
def highlight_commands(self) -> typing.List[str]: def highlight_commands(self) -> typing.List[str]:
commands: typing.List[str] = [] commands: typing.List[str] = []
directory_marker = self.vim.call(
'escape', self._directory_marker, r'~/\.^$[]*')
commands.append(
r'syntax match {0}_{1} /{2}/ conceal contained '
'containedin={0}_directory'.format(
self.syntax_name, 'directory_marker', directory_marker))
commands.append(
r'syntax match {0}_{1} /{2}\%(.{3}[{4}/]\)\+/ '
'contained containedin={0}'.format(
self.syntax_name, 'directory', directory_marker, r'\{-}',
'\\' if self.vim.call('defx#util#is_windows') else ''))
file_marker = self.vim.call(
'escape', self._file_marker, r'~/\.^$[]*')
commands.append(
r'syntax match {0}_{1} /{2}/ conceal contained '
'containedin={0}_file'.format(
self.syntax_name, 'file_marker', file_marker))
commands.append(
r'syntax match {0}_{1} /{2}.{3}/ '
'contained containedin={0}'.format(
self.syntax_name, 'file', file_marker, r'\{-}'))
root_marker = self.vim.call(
'escape', self._context.root_marker, r'~/\.^$[]*')
commands.append(
r'syntax match {0}_{1} /{2}/ contained '
'containedin={0}_root'.format(
self.syntax_name, 'root_marker', root_marker))
commands.append(
r'syntax match {0}_{1} /{2}.*/ contained '
'containedin={0}'.format(
self.syntax_name, 'root', root_marker))
commands.append( commands.append(
'highlight default link {}_{} {}'.format( 'highlight default link {}_{} {}'.format(
self.syntax_name, 'directory', 'PreProc')) self.highlight_name, 'directory', 'PreProc'))
commands.append( commands.append(
'highlight default link {}_{} {}'.format( 'highlight default link {}_{} {}'.format(
self.syntax_name, 'hidden', 'Comment')) self.highlight_name, 'root_marker',
commands.append(
'highlight default link {}_{} {}'.format(
self.syntax_name, 'root_marker',
self.vars['root_marker_highlight'])) self.vars['root_marker_highlight']))
commands.append( commands.append(
'highlight default link {}_{} {}'.format( 'highlight default link {}_{} {}'.format(
self.syntax_name, 'root', 'Identifier')) self.highlight_name, 'root', 'Identifier'))
return commands return commands
def _strwidth(self, word: str) -> int:
return (int(self.vim.call('strwidth', word))
if len(word) != len(bytes(word, 'utf-8',
'surrogatepass')) else len(word))
def _truncate(self, word: str) -> str: def _truncate(self, word: str) -> str:
width = self._strwidth(word) width = strwidth(self.vim, word)
max_length = self._current_length max_length = self._current_length
if (width > max_length or if (width > max_length or
len(word) != len(bytes(word, 'utf-8', 'surrogatepass'))): len(word) != len(bytes(word, 'utf-8', 'surrogatepass'))):

View File

@ -5,9 +5,9 @@
# License: MIT license # License: MIT license
# ============================================================================ # ============================================================================
from defx.base.column import Base from defx.base.column import Base, Highlights
from defx.context import Context from defx.context import Context
from defx.util import Nvim from defx.util import Nvim, Candidate, len_bytes
import typing import typing
@ -24,23 +24,36 @@ class Column(Base):
'opened_icon': '-', 'opened_icon': '-',
'root_icon': ' ', 'root_icon': ' ',
} }
self.has_get_with_highlights = True
self._syntaxes = [ self._syntaxes = [
'directory_icon', 'directory_icon',
'opened_icon', 'opened_icon',
'root_icon', 'root_icon',
] ]
self._highlights = {
'directory': 'Special',
'opened': 'Special',
'root': 'Identifier',
}
def get(self, context: Context, def get_with_highlights(
candidate: typing.Dict[str, typing.Any]) -> str: self, context: Context, candidate: Candidate
icon: str = ' ' ) -> typing.Tuple[str, Highlights]:
if candidate['is_opened_tree']: if candidate['is_opened_tree']:
icon = self.vars['opened_icon'] return (self.vars['opened_icon'],
[(f'{self.highlight_name}_opened_icon',
self.start, len_bytes(self.vars['opened_icon']))])
elif candidate['is_root']: elif candidate['is_root']:
icon = self.vars['root_icon'] return (self.vars['root_icon'],
[(f'{self.highlight_name}_root_icon',
self.start, len_bytes(self.vars['root_icon']))])
elif candidate['is_directory']: elif candidate['is_directory']:
icon = self.vars['directory_icon'] return (self.vars['directory_icon'],
[(f'{self.highlight_name}_directory_icon',
self.start, len_bytes(self.vars['directory_icon']))])
return icon return (' ', [])
def length(self, context: Context) -> int: def length(self, context: Context) -> int:
return typing.cast(int, self.vars['length']) return typing.cast(int, self.vars['length'])
@ -50,19 +63,9 @@ class Column(Base):
def highlight_commands(self) -> typing.List[str]: def highlight_commands(self) -> typing.List[str]:
commands: typing.List[str] = [] commands: typing.List[str] = []
for icon, highlight in { for icon, highlight in self._highlights.items():
'directory': 'Special',
'opened': 'Special',
'root': 'Identifier',
}.items():
commands.append(
('syntax match {0}_{1}_icon /[{2}]{3}/ ' +
'contained containedin={0}').format(
self.syntax_name, icon, self.vars[icon + '_icon'],
' ' if self.is_within_variable else ''
))
commands.append( commands.append(
'highlight default link {}_{}_icon {}'.format( 'highlight default link {}_{}_icon {}'.format(
self.syntax_name, icon, highlight)) self.highlight_name, icon, highlight))
return commands return commands

View File

@ -4,9 +4,9 @@
# License: MIT license # License: MIT license
# ============================================================================ # ============================================================================
from defx.base.column import Base from defx.base.column import Base, Highlights
from defx.context import Context from defx.context import Context
from defx.util import Nvim from defx.util import Nvim, Candidate, len_bytes
import os import os
import typing import typing
@ -29,15 +29,25 @@ class Column(Base):
'readonly', 'readonly',
'selected', 'selected',
] ]
self.has_get_with_highlights = True
def get(self, context: Context, self._icons = {
candidate: typing.Dict[str, typing.Any]) -> str: 'readonly': 'Comment',
icon: str = ' ' * self.vars['length'] 'selected': 'Statement',
}
def get_with_highlights(
self, context: Context, candidate: Candidate
) -> typing.Tuple[str, Highlights]:
if candidate['is_selected']: if candidate['is_selected']:
icon = self.vars['selected_icon'] return (str(self.vars['selected_icon']),
[(f'{self.highlight_name}_selected',
self.start, len_bytes(self.vars['selected_icon']))])
elif not os.access(str(candidate['action__path']), os.W_OK): elif not os.access(str(candidate['action__path']), os.W_OK):
icon = self.vars['readonly_icon'] return (str(self.vars['readonly_icon']),
return icon [(f'{self.highlight_name}_readonly',
self.start, len_bytes(self.vars['readonly_icon']))])
return (' ' * self.vars['length'], [])
def length(self, context: Context) -> int: def length(self, context: Context) -> int:
return typing.cast(int, self.vars['length']) return typing.cast(int, self.vars['length'])
@ -47,15 +57,8 @@ class Column(Base):
def highlight_commands(self) -> typing.List[str]: def highlight_commands(self) -> typing.List[str]:
commands: typing.List[str] = [] commands: typing.List[str] = []
for icon, highlight in { for icon, highlight in self._icons.items():
'readonly': 'Comment',
'selected': 'Statement',
}.items():
commands.append(
('syntax match {0}_{1} /[{2}]/ ' +
'contained containedin={0}').format(
self.syntax_name, icon, self.vars[icon + '_icon']))
commands.append( commands.append(
'highlight default link {}_{} {}'.format( 'highlight default link {}_{} {}'.format(
self.syntax_name, icon, highlight)) self.highlight_name, icon, highlight))
return commands return commands

View File

@ -4,9 +4,9 @@
# License: MIT license # License: MIT license
# ============================================================================ # ============================================================================
from defx.base.column import Base from defx.base.column import Base, Highlights
from defx.context import Context from defx.context import Context
from defx.util import Nvim, readable from defx.util import Nvim, readable, Candidate
import typing import typing
@ -17,14 +17,18 @@ class Column(Base):
super().__init__(vim) super().__init__(vim)
self.name = 'size' self.name = 'size'
self.has_get_with_highlights = True
self._length = 9
def get(self, context: Context, def get_with_highlights(
candidate: typing.Dict[str, typing.Any]) -> str: self, context: Context, candidate: Candidate
) -> typing.Tuple[str, Highlights]:
path = candidate['action__path'] path = candidate['action__path']
if not readable(path) or path.is_dir(): if not readable(path) or path.is_dir():
return ' ' * 9 return (' ' * self._length, [])
size = self._get_size(path.stat().st_size) size = self._get_size(path.stat().st_size)
return '{:>6s}{:>3s}'.format(size[0], size[1]) text = '{:>6s}{:>3s}'.format(size[0], size[1])
return (text, [(self.highlight_name, self.start, self._length)])
def _get_size(self, size: float) -> typing.Tuple[str, str]: def _get_size(self, size: float) -> typing.Tuple[str, str]:
multiple = 1024 multiple = 1024
@ -38,10 +42,10 @@ class Column(Base):
return ('INF', '') return ('INF', '')
def length(self, context: Context) -> int: def length(self, context: Context) -> int:
return 9 return self._length
def highlight_commands(self) -> typing.List[str]: def highlight_commands(self) -> typing.List[str]:
commands: typing.List[str] = [] commands: typing.List[str] = []
commands.append( commands.append(
f'highlight default link {self.syntax_name} Constant') f'highlight default link {self.highlight_name} Constant')
return commands return commands

View File

@ -4,9 +4,9 @@
# License: MIT license # License: MIT license
# ============================================================================ # ============================================================================
from defx.base.column import Base from defx.base.column import Base, Highlights
from defx.context import Context from defx.context import Context
from defx.util import Nvim, readable from defx.util import Nvim, readable, Candidate
from defx.view import View from defx.view import View
import time import time
@ -19,22 +19,26 @@ class Column(Base):
super().__init__(vim) super().__init__(vim)
self.name = 'time' self.name = 'time'
self._length = 0
self.vars = { self.vars = {
'format': '%y.%m.%d %H:%M', 'format': '%y.%m.%d %H:%M',
} }
self.has_get_with_highlights = True
self._length = 0
def on_init(self, view: View, context: Context) -> None: def on_init(self, view: View, context: Context) -> None:
self._length = self.vim.call('strwidth', self._length = self.vim.call('strwidth',
time.strftime(self.vars['format'])) time.strftime(self.vars['format']))
def get(self, context: Context, def get_with_highlights(
candidate: typing.Dict[str, typing.Any]) -> str: self, context: Context, candidate: Candidate
) -> typing.Tuple[str, Highlights]:
path = candidate['action__path'] path = candidate['action__path']
if not readable(path): if not readable(path):
return str(' ' * self._length) return (str(' ' * self._length), [])
return time.strftime(self.vars['format'], text = time.strftime(self.vars['format'],
time.localtime(path.stat().st_mtime)) time.localtime(path.stat().st_mtime))
return (text, [(self.highlight_name, self.start, self._length)])
def length(self, context: Context) -> int: def length(self, context: Context) -> int:
return self._length return self._length
@ -42,5 +46,5 @@ class Column(Base):
def highlight_commands(self) -> typing.List[str]: def highlight_commands(self) -> typing.List[str]:
commands: typing.List[str] = [] commands: typing.List[str] = []
commands.append( commands.append(
f'highlight default link {self.syntax_name} Identifier') f'highlight default link {self.highlight_name} Identifier')
return commands return commands

View File

@ -4,12 +4,11 @@
# License: MIT license # License: MIT license
# ============================================================================ # ============================================================================
from defx.base.column import Base from defx.base.column import Base, Highlights
from defx.context import Context from defx.context import Context
from defx.util import Nvim from defx.util import Nvim, Candidate, len_bytes
from defx.view import View from defx.view import View
import re
import typing import typing
@ -21,7 +20,7 @@ class Column(Base):
self.name = 'type' self.name = 'type'
types = [ types = [
{ {
'name': 'text', 'globs': ['*.txt'], 'name': 'text', 'globs': ['*.txt', '*.md', 'README'],
'icon': '[T]', 'highlight': 'Constant' 'icon': '[T]', 'highlight': 'Constant'
}, },
{ {
@ -40,19 +39,27 @@ class Column(Base):
self.vars = { self.vars = {
'types': types, 'types': types,
} }
self.has_get_with_highlights = True
self._length: int = 0 self._length: int = 0
def on_init(self, view: View, context: Context) -> None: def on_init(self, view: View, context: Context) -> None:
self._length = max([self.vim.call('strwidth', x['icon']) self._length = max([self.vim.call('strwidth', x['icon'])
for x in self.vars['types']]) for x in self.vars['types']])
def get(self, context: Context, def get_with_highlights(
candidate: typing.Dict[str, typing.Any]) -> str: self, context: Context, candidate: Candidate
) -> typing.Tuple[str, Highlights]:
for t in self.vars['types']: for t in self.vars['types']:
for glob in t['globs']: for glob in t['globs']:
if candidate['action__path'].match(glob): if not candidate['action__path'].match(glob):
return str(t['icon']) continue
return ' ' * self._length return (str(t['icon']), [
(f"{self.highlight_name}_{t['name']}",
self.start, len_bytes(t['icon']))
])
return (' ' * self._length, [])
def length(self, context: Context) -> int: def length(self, context: Context) -> int:
return self._length return self._length
@ -64,11 +71,7 @@ class Column(Base):
def highlight_commands(self) -> typing.List[str]: def highlight_commands(self) -> typing.List[str]:
commands: typing.List[str] = [] commands: typing.List[str] = []
for t in self.vars['types']: for t in self.vars['types']:
commands.append(
('syntax match {0}_{1} /{2}/ ' +
'contained containedin={0}').format(
self.syntax_name, t['name'], re.escape(t['icon'])))
commands.append( commands.append(
'highlight default link {}_{} {}'.format( 'highlight default link {}_{} {}'.format(
self.syntax_name, t['name'], t['highlight'])) self.highlight_name, t['name'], t['highlight']))
return commands return commands

View File

@ -12,31 +12,39 @@ class Context(typing.NamedTuple):
auto_cd: bool = False auto_cd: bool = False
auto_recursive_level: int = 0 auto_recursive_level: int = 0
buffer_name: str = 'default' buffer_name: str = 'default'
close: bool = False
columns: str = '' columns: str = ''
cursor: int = 0 cursor: int = 0
direction: str = '' direction: str = ''
drives: typing.List[str] = [] drives: typing.List[str] = []
filtered_files: str = ''
focus: bool = False
floating_preview: bool = False
ignored_files: str = '' ignored_files: str = ''
listed: bool = False listed: bool = False
new: bool = False new: bool = False
prev_bufnr: int = 0 prev_bufnr: int = 0
prev_last_bufnr: int = 0 prev_last_bufnr: int = 0
prev_winid: int = 0 prev_winid: int = 0
preview_height: int = 0
preview_width: int = 0
profile: bool = False profile: bool = False
resume: bool = False resume: bool = False
root_marker: str = '' root_marker: str = ''
search: str = '' search: str = ''
session_file: str = '' session_file: str = ''
sort: str = ''
show_ignored_files: bool = False show_ignored_files: bool = False
sort: str = ''
split: str = 'no' split: str = 'no'
toggle: bool = False
targets: typing.List[typing.Dict[str, typing.Any]] = [] targets: typing.List[typing.Dict[str, typing.Any]] = []
toggle: bool = False
variable_length: int = 0 variable_length: int = 0
visual_start: int = 0
visual_end: int = 0 visual_end: int = 0
visual_start: int = 0
with_highlights: bool = True
wincol: int = 0 wincol: int = 0
winheight: int = 0 winheight: int = 0
winrelative: str = 'editor' winrelative: str = 'editor'
winrow: int = 0 winrow: int = 0
winwidth: int = 0 winwidth: int = 0
vertical_preview: bool = False

View File

@ -6,7 +6,9 @@
import typing import typing
from defx.source.file import Source as File from defx.base.source import Base as Source
from defx.source.file.list import Source as SourceList
from defx.source.file import Source as SourceFile
from defx.context import Context from defx.context import Context
from defx.sort import sort from defx.sort import sort
from defx.util import Nvim from defx.util import Nvim
@ -20,20 +22,25 @@ Candidate = typing.Dict[str, typing.Any]
class Defx(object): class Defx(object):
def __init__(self, vim: Nvim, context: Context, def __init__(self, vim: Nvim, context: Context,
cwd: str, index: int) -> None: source_name: str, cwd: str, index: int) -> None:
self._vim = vim self._vim = vim
self._context = context self._context = context
self._cwd = self._vim.call('getcwd') self._cwd = self._vim.call('getcwd')
self.cd(cwd) self.cd(cwd)
self._source: File = File(self._vim)
self._source: Source = (SourceList(self._vim)
if source_name == 'file/list'
else SourceFile(self._vim))
self._index = index self._index = index
self._enabled_ignored_files = not context.show_ignored_files self._enabled_ignored_files = not context.show_ignored_files
self._filtered_files = context.filtered_files.split(',')
self._ignored_files = context.ignored_files.split(',') self._ignored_files = context.ignored_files.split(',')
self._cursor_history: typing.Dict[str, Path] = {} self._cursor_history: typing.Dict[str, Path] = {}
self._sort_method: str = self._context.sort self._sort_method: str = self._context.sort
self._mtime: int = -1 self._mtime: int = -1
self._opened_candidates: typing.Set[str] = set() self._opened_candidates: typing.Set[str] = set()
self._selected_candidates: typing.Set[str] = set() self._selected_candidates: typing.Set[str] = set()
self._nested_candidates: typing.Set[str] = set()
self._init_source() self._init_source()
@ -49,19 +56,24 @@ class Defx(object):
def cd(self, path: str) -> None: def cd(self, path: str) -> None:
self._cwd = str(Path(self._cwd).joinpath(path)) self._cwd = str(Path(self._cwd).joinpath(path))
if self._context.auto_cd: if self._context.auto_cd and Path(path).is_dir():
cd(self._vim, path) cd(self._vim, path)
def get_root_candidate(self) -> Candidate: def get_root_candidate(self) -> Candidate:
""" """
Returns root candidate Returns root candidate
""" """
if not self._source:
return {}
root = self._source.get_root_candidate(self._context, Path(self._cwd)) root = self._source.get_root_candidate(self._context, Path(self._cwd))
root['is_root'] = True root['is_root'] = True
root['is_opened_tree'] = False root['is_opened_tree'] = False
root['is_selected'] = False root['is_selected'] = False
root['level'] = 0 root['level'] = 0
root['word'] = self._context.root_marker + root['word'] root['root_marker'] = self._context.root_marker + (
self._source.name + ':' if self._source.name != 'file' else '')
root['word'] = root['root_marker'] + root['word']
return root return root
@ -71,21 +83,26 @@ class Defx(object):
gathered_candidates = self.gather_candidates_recursive( gathered_candidates = self.gather_candidates_recursive(
path, base_level, max_level) path, base_level, max_level)
if self._opened_candidates: if not self._opened_candidates and not self._nested_candidates:
candidates = [] return gathered_candidates
for candidate in gathered_candidates:
candidates.append(candidate)
candidate['level'] = base_level
candidate_path = str(candidate['action__path'])
if (candidate_path in self._opened_candidates and candidates = []
not candidate['is_opened_tree']): for candidate in gathered_candidates:
candidate['is_opened_tree'] = True candidates.append(candidate)
candidates += self.tree_candidates( candidate['level'] = base_level
candidate_path, base_level + 1, max_level) candidate_path = str(candidate['action__path'])
else:
candidates = gathered_candidates
if not candidate['is_directory']:
continue
if (candidate_path not in self._opened_candidates and
candidate_path not in self._nested_candidates):
continue
children = self.tree_candidates(
candidate_path, base_level + 1, max_level)
candidate['is_opened_tree'] = True
candidates += children
return candidates return candidates
def gather_candidates_recursive( def gather_candidates_recursive(
@ -110,9 +127,24 @@ class Defx(object):
""" """
Returns file candidates Returns file candidates
""" """
if not self._source:
return []
candidates = self._source.gather_candidates( candidates = self._source.gather_candidates(
self._context, Path(path)) self._context, Path(path))
if self._filtered_files != ['']:
new_candidates = []
for candidate in candidates:
matched = False
for glob in self._filtered_files:
if candidate['action__path'].match(glob):
matched = True
break
if matched or candidate['is_directory']:
new_candidates.append(candidate)
candidates = new_candidates
if self._enabled_ignored_files: if self._enabled_ignored_files:
for glob in self._ignored_files: for glob in self._ignored_files:
candidates = [x for x in candidates candidates = [x for x in candidates

View File

@ -4,9 +4,10 @@
# License: MIT license # License: MIT license
# ============================================================================ # ============================================================================
from pathlib import Path
import copy import copy
import importlib import importlib
from pathlib import Path import mimetypes
import shutil import shutil
import time import time
import typing import typing
@ -17,7 +18,7 @@ from defx.base.kind import Base
from defx.clipboard import ClipboardAction from defx.clipboard import ClipboardAction
from defx.context import Context from defx.context import Context
from defx.defx import Defx from defx.defx import Defx
from defx.util import cd, cwd_input, confirm, error from defx.util import cd, cwd_input, confirm, error, Candidate
from defx.util import readable, Nvim from defx.util import readable, Nvim
from defx.view import View from defx.view import View
@ -50,6 +51,9 @@ class Kind(Base):
def check_overwrite(view: View, dest: Path, src: Path) -> Path: def check_overwrite(view: View, dest: Path, src: Path) -> Path:
if not src.exists() or not dest.exists():
return Path('')
s_stat = src.stat() s_stat = src.stat()
s_mtime = s_stat.st_mtime s_mtime = s_stat.st_mtime
view.print_msg(f' src: {src} {s_stat.st_size} bytes') view.print_msg(f' src: {src} {s_stat.st_size} bytes')
@ -68,8 +72,10 @@ def check_overwrite(view: View, dest: Path, src: Path) -> Path:
elif choice == 2: elif choice == 2:
ret = Path('') ret = Path('')
elif choice == 3: elif choice == 3:
ret = Path(view._vim.call('input', f'{src} -> ', str(dest), ret = Path(view._vim.call(
('dir' if src.is_dir() else 'file'))) 'defx#util#input',
f'{src} -> ', str(dest),
('dir' if src.is_dir() else 'file')))
elif choice == 4 and d_mtime < s_mtime: elif choice == 4 and d_mtime < s_mtime:
ret = src ret = src
elif choice == 5: elif choice == 5:
@ -82,14 +88,22 @@ def _cd(view: View, defx: Defx, context: Context) -> None:
""" """
Change the current directory. Change the current directory.
""" """
path = Path(context.args[0]) if context.args else Path.home() source_name = defx._source.name
if context.args:
if len(context.args) > 1:
source_name = context.args[0]
path = Path(context.args[1])
else:
path = Path(context.args[0])
else:
path = Path.home()
path = Path(defx._cwd).joinpath(path).resolve() path = Path(defx._cwd).joinpath(path).resolve()
if not readable(path) or not path.is_dir(): if not readable(path) or (source_name == 'file' and not path.is_dir()):
error(view._vim, f'{path} is not readable directory') error(view._vim, f'{path} is invalid.')
return return
prev_cwd = defx._cwd prev_cwd = defx._cwd
view.cd(defx, str(path), context.cursor) view.cd(defx, source_name, str(path), context.cursor)
if context.args and context.args[0] == '..': if context.args and context.args[0] == '..':
view.search_file(Path(prev_cwd), defx._index) view.search_file(Path(prev_cwd), defx._index)
@ -132,14 +146,14 @@ def _drop(view: View, defx: Defx, context: Context) -> None:
""" """
Open like :drop. Open like :drop.
""" """
cwd = view._vim.call('getcwd') cwd = view._vim.call('getcwd', -1)
command = context.args[0] if context.args else 'edit' command = context.args[0] if context.args else 'edit'
for target in context.targets: for target in context.targets:
path = target['action__path'] path = target['action__path']
if path.is_dir(): if path.is_dir():
view.cd(defx, str(path), context.cursor) view.cd(defx, defx._source.name, str(path), context.cursor)
continue continue
bufnr = view._vim.call('bufnr', f'^{path}$') bufnr = view._vim.call('bufnr', f'^{path}$')
@ -153,12 +167,18 @@ def _drop(view: View, defx: Defx, context: Context) -> None:
view._vim.call('win_gotoid', context.prev_winid) view._vim.call('win_gotoid', context.prev_winid)
else: else:
view._vim.command('wincmd w') view._vim.command('wincmd w')
try:
path = path.relative_to(cwd) if not view._vim.call('haslocaldir'):
except ValueError: try:
pass path = path.relative_to(cwd)
except ValueError:
pass
view._vim.call('defx#util#execute_path', command, str(path)) view._vim.call('defx#util#execute_path', command, str(path))
view.restore_previous_buffer()
view.close_preview()
@action(name='execute_command', attr=ActionAttr.NO_TAGETS) @action(name='execute_command', attr=ActionAttr.NO_TAGETS)
def _execute_command(view: View, defx: Defx, context: Context) -> None: def _execute_command(view: View, defx: Defx, context: Context) -> None:
@ -231,7 +251,7 @@ def _new_directory(view: View, defx: Defx, context: Context) -> None:
filename.mkdir(parents=True) filename.mkdir(parents=True)
view.redraw(True) view.redraw(True)
view.search_file(filename, defx._index) view.search_recursive(filename, defx._index)
@action(name='new_file') @action(name='new_file')
@ -269,7 +289,7 @@ def _new_file(view: View, defx: Defx, context: Context) -> None:
filename.touch() filename.touch()
view.redraw(True) view.redraw(True)
view.search_file(filename, defx._index) view.search_recursive(filename, defx._index)
@action(name='new_multiple_files') @action(name='new_multiple_files')
@ -312,7 +332,7 @@ def _new_multiple_files(view: View, defx: Defx, context: Context) -> None:
filename.touch() filename.touch()
view.redraw(True) view.redraw(True)
view.search_file(filename, defx._index) view.search_recursive(filename, defx._index)
@action(name='open') @action(name='open')
@ -320,21 +340,32 @@ def _open(view: View, defx: Defx, context: Context) -> None:
""" """
Open the file. Open the file.
""" """
cwd = view._vim.call('getcwd') cwd = view._vim.call('getcwd', -1)
command = context.args[0] if context.args else 'edit' command = context.args[0] if context.args else 'edit'
previewed_buffers = view._vim.vars['defx#_previewed_buffers']
for target in context.targets: for target in context.targets:
path = target['action__path'] path = target['action__path']
if path.is_dir(): if path.is_dir():
view.cd(defx, str(path), context.cursor) view.cd(defx, defx._source.name, str(path), context.cursor)
continue continue
try: if not view._vim.call('haslocaldir'):
path = path.relative_to(cwd) try:
except ValueError: path = path.relative_to(cwd)
pass except ValueError:
pass
view._vim.call('defx#util#execute_path', command, str(path)) view._vim.call('defx#util#execute_path', command, str(path))
bufnr = str(view._vim.call('bufnr', str(path)))
if bufnr in previewed_buffers:
previewed_buffers.pop(bufnr)
view._vim.vars['defx#_previewed_buffers'] = previewed_buffers
view.restore_previous_buffer()
view.close_preview()
@action(name='open_directory') @action(name='open_directory')
def _open_directory(view: View, defx: Defx, context: Context) -> None: def _open_directory(view: View, defx: Defx, context: Context) -> None:
@ -348,7 +379,7 @@ def _open_directory(view: View, defx: Defx, context: Context) -> None:
path = target['action__path'] path = target['action__path']
if path.is_dir(): if path.is_dir():
view.cd(defx, str(path), context.cursor) view.cd(defx, 'file', str(path), context.cursor)
@action(name='paste', attr=ActionAttr.NO_TAGETS) @action(name='paste', attr=ActionAttr.NO_TAGETS)
@ -373,7 +404,7 @@ def _paste(view: View, defx: Defx, context: Context) -> None:
continue continue
dest = overwrite dest = overwrite
if path == dest: if not path.exists() or path == dest:
continue continue
view.print_msg( view.print_msg(
@ -384,13 +415,89 @@ def _paste(view: View, defx: Defx, context: Context) -> None:
else: else:
shutil.copy2(str(path), dest) shutil.copy2(str(path), dest)
elif action == ClipboardAction.MOVE: elif action == ClipboardAction.MOVE:
if dest.exists():
# Must remove dest before
if dest.is_dir():
shutil.rmtree(str(dest))
else:
dest.unlink()
shutil.move(str(path), cwd) shutil.move(str(path), cwd)
view._vim.command('redraw') view._vim.command('redraw')
if action == ClipboardAction.MOVE:
# Clear clipboard after move
view._clipboard.candidates = []
view._vim.command('echo') view._vim.command('echo')
view.redraw(True) view.redraw(True)
if dest: if dest:
view.search_file(dest, defx._index) view.search_recursive(dest, defx._index)
@action(name='preview')
def _preview(view: View, defx: Defx, context: Context) -> None:
candidate = view.get_cursor_candidate(context.cursor)
if not candidate or candidate['action__path'].is_dir():
return
filepath = str(candidate['action__path'])
guess_type = mimetypes.guess_type(filepath)[0]
if (guess_type and guess_type.startswith('image/') and
shutil.which('ueberzug') and shutil.which('bash')):
_preview_image(view, defx, context, candidate)
return
_preview_file(view, defx, context, candidate)
def _preview_file(view: View, defx: Defx,
context: Context, candidate: Candidate) -> None:
filepath = str(candidate['action__path'])
has_preview = bool(view._vim.call('defx#util#_get_preview_window'))
if (has_preview and view._previewed_target and
view._previewed_target == candidate):
view._vim.command('pclose!')
return
prev_id = view._vim.call('win_getid')
listed = view._vim.call('buflisted', filepath)
view._previewed_target = candidate
view._vim.call('defx#util#preview_file',
context._replace(targets=[])._asdict(), filepath)
view._vim.current.window.options['foldenable'] = False
if not listed:
bufnr = str(view._vim.call('bufnr', filepath))
previewed_buffers = view._vim.vars['defx#_previewed_buffers']
previewed_buffers[bufnr] = 1
view._vim.vars['defx#_previewed_buffers'] = previewed_buffers
view._vim.call('win_gotoid', prev_id)
def _preview_image(view: View, defx: Defx,
context: Context, candidate: Candidate) -> None:
has_nvim = view._vim.call('has', 'nvim')
filepath = str(candidate['action__path'])
preview_image_sh = Path(__file__).parent.parent.joinpath(
'preview_image.sh')
if has_nvim:
jobfunc = 'jobstart'
jobopts = {}
else:
jobfunc = 'job_start'
jobopts = {'in_io': 'null', 'out_io': 'null', 'err_io': 'null'}
wincol = context.wincol + view._vim.call('winwidth', 0)
if wincol + context.preview_width > view._vim.options['columns']:
wincol -= 2 * context.preview_width
args = ['bash', str(preview_image_sh), filepath,
wincol, 1, context.preview_width]
view._vim.call(jobfunc, args, jobopts)
@action(name='remove', attr=ActionAttr.REDRAW) @action(name='remove', attr=ActionAttr.REDRAW)
@ -418,8 +525,11 @@ def _remove(view: View, defx: Defx, context: Context) -> None:
else: else:
path.unlink() path.unlink()
view._vim.call('defx#util#buffer_delete',
view._vim.call('bufnr', str(path)))
@action(name='remove_trash')
@action(name='remove_trash', attr=ActionAttr.REDRAW)
def _remove_trash(view: View, defx: Defx, context: Context) -> None: def _remove_trash(view: View, defx: Defx, context: Context) -> None:
""" """
Delete the file or directory. Delete the file or directory.
@ -443,7 +553,9 @@ def _remove_trash(view: View, defx: Defx, context: Context) -> None:
import send2trash import send2trash
for target in context.targets: for target in context.targets:
send2trash.send2trash(str(target['action__path'])) send2trash.send2trash(str(target['action__path']))
view.redraw(True)
view._vim.call('defx#util#buffer_delete',
view._vim.call('bufnr', str(target['action__path'])))
@action(name='rename') @action(name='rename')
@ -475,7 +587,13 @@ def _rename(view: View, defx: Defx, context: Context) -> None:
error(view._vim, f'{new} already exists') error(view._vim, f'{new} already exists')
continue continue
if not new.parent.exists():
new.parent.mkdir(parents=True)
old.rename(new) old.rename(new)
# Check rename
view._vim.call('defx#util#buffer_rename',
view._vim.call('bufnr', str(old)), str(new))
view.redraw(True) view.redraw(True)
view.search_file(new, defx._index) view.search_recursive(new, defx._index)

View File

@ -0,0 +1,17 @@
import ueberzug.lib.v0 as ueberzug
import sys
import time
if __name__ == '__main__' and len(sys.argv) > 3:
with ueberzug.Canvas() as c:
total_width = int(sys.argv[2])
preview_width = int(sys.argv[3])
ratio = preview_width / total_width
width = preview_width
demo = c.create_placement(
'demo', x=(total_width - preview_width) * ratio, y=1,
width=width, scaler=ueberzug.ScalerOption.COVER.value)
demo.path = sys.argv[1]
demo.visibility = ueberzug.Visibility.VISIBLE
time.sleep(1)

View File

@ -0,0 +1,4 @@
{
declare -pA addCommand=([action]="add" [identifier]="defx_preview" [x]="$2" [y]="$3" [width]="$4" [path]="$1")
sleep 5
} | ueberzug layer --parser bash

View File

@ -23,13 +23,8 @@ class Rplugin:
def start(self, args: typing.List[typing.Any]) -> None: def start(self, args: typing.List[typing.Any]) -> None:
[paths, context] = args [paths, context] = args
views = [x for x in self._views self.get_view(context).init_paths(
if context['buffer_name'] == x._context.buffer_name] paths, context, self._clipboard)
if not views or context['new']:
view = View(self._vim, len(self._views))
views = [view]
self._views.append(view)
views[0].init(paths, context, self._clipboard)
def do_action(self, args: typing.List[typing.Any]) -> None: def do_action(self, args: typing.List[typing.Any]) -> None:
views = [x for x in self._views views = [x for x in self._views
@ -67,6 +62,15 @@ class Rplugin:
return view._context._asdict() return view._context._asdict()
return {} return {}
def get_view(self, context: typing.Dict[str, typing.Any]) -> View:
views = [x for x in self._views
if context['buffer_name'] == x._context.buffer_name]
if not views or context['new']:
view = View(self._vim, len(self._views))
views = [view]
self._views.append(view)
return views[0]
def redraw(self, views: typing.List[View]) -> None: def redraw(self, views: typing.List[View]) -> None:
call = self._vim.call call = self._vim.call
for view in [x for x in views for view in [x for x in views

View File

@ -51,11 +51,14 @@ class Source(Base):
if not readable(path) or not path.is_dir(): if not readable(path) or not path.is_dir():
error(self.vim, f'"{path}" is not readable directory.') error(self.vim, f'"{path}" is not readable directory.')
return [] return []
for entry in path.iterdir(): try:
candidates.append({ for entry in path.iterdir():
'word': entry.name.replace('\n', '\\n') + ( candidates.append({
'/' if safe_call(entry.is_dir, False) else ''), 'word': entry.name.replace('\n', '\\n') + (
'is_directory': safe_call(entry.is_dir, False), '/' if safe_call(entry.is_dir, False) else ''),
'action__path': entry, 'is_directory': safe_call(entry.is_dir, False),
}) 'action__path': entry,
})
except OSError:
pass
return candidates return candidates

View File

@ -0,0 +1,71 @@
# ============================================================================
# FILE: file/list.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================
from pathlib import Path
import typing
from defx.base.source import Base
from defx.source.file import Source as File
from defx.context import Context
from defx.util import error, readable, safe_call, Nvim
class Source(Base):
def __init__(self, vim: Nvim) -> None:
super().__init__(vim)
self.name = 'file/list'
from defx.kind.file import Kind
self.kind: Kind = Kind(self.vim)
self.vars = {
'root': None,
}
def get_root_candidate(
self, context: Context, path: Path
) -> typing.Dict[str, typing.Any]:
word = self.vim.call('fnamemodify', str(path), ':~')
if self.vim.call('defx#util#is_windows'):
word = word.replace('\\', '/')
if word[-1:] != '/':
word += '/'
if self.vars['root']:
word = self.vim.call(self.vars['root'], str(path))
word = word.replace('\n', '\\n')
return {
'word': word,
'is_directory': False,
'action__path': path,
}
def gather_candidates(
self, context: Context, path: Path
) -> typing.List[typing.Dict[str, typing.Any]]:
if not readable(path):
error(self.vim, f'"{path}" is not readable file.')
return []
if path.is_dir():
# Fallback to file source
return File(self.vim).gather_candidates(context, path)
candidates = []
with path.open() as f:
for line in f:
entry = Path(line.rstrip('\n'))
if not entry.exists():
continue
candidates.append({
'word': str(entry) + (
'/' if safe_call(entry.is_dir, False) else ''),
'is_directory': safe_call(entry.is_dir, False),
'action__path': entry,
})
return candidates

View File

@ -4,11 +4,13 @@
# License: MIT license # License: MIT license
# ============================================================================ # ============================================================================
import importlib.util
import os
import typing
from pathlib import Path from pathlib import Path
from pynvim import Nvim from pynvim import Nvim
from sys import executable, base_exec_prefix
import importlib.util
import os
import shutil
import typing
UserContext = typing.Dict[str, typing.Any] UserContext = typing.Dict[str, typing.Any]
Candidate = typing.Dict[str, typing.Any] Candidate = typing.Dict[str, typing.Any]
@ -39,7 +41,9 @@ def error(vim: Nvim, expr: typing.Any) -> None:
""" """
Prints the error messages to Vim/Nvim's :messages buffer. Prints the error messages to Vim/Nvim's :messages buffer.
""" """
vim.call('defx#util#print_error', expr) if isinstance(expr, set):
expr = [str(x) for x in expr]
vim.call('defx#util#print_error', str(expr))
def confirm(vim: Nvim, question: str) -> bool: def confirm(vim: Nvim, question: str) -> bool:
@ -88,3 +92,34 @@ def safe_call(fn: typing.Callable[..., typing.Any],
return fn() return fn()
except OSError: except OSError:
return fallback return fallback
def get_python_exe() -> str:
if 'py' in str(Path(executable).name):
return executable
for exe in ['python3', 'python']:
which = shutil.which(exe)
if which is not None:
return which
for name in (Path(base_exec_prefix).joinpath(v) for v in [
'python3', 'python',
str(Path('bin').joinpath('python3')),
str(Path('bin').joinpath('python')),
]):
if name.exists():
return str(name)
# return sys.executable anyway. This may not work on windows
return executable
def strwidth(vim: Nvim, word: str) -> int:
return (int(vim.call('strwidth', word))
if len(word) != len(bytes(word, 'utf-8',
'surrogatepass')) else len(word))
def len_bytes(word: str) -> int:
return len(bytes(word, 'utf-8', 'surrogatepass'))

View File

@ -14,7 +14,10 @@ from defx.clipboard import Clipboard
from defx.context import Context from defx.context import Context
from defx.defx import Defx from defx.defx import Defx
from defx.session import Session from defx.session import Session
from defx.util import error, import_plugin, safe_call, Nvim 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): class View(object):
@ -34,26 +37,65 @@ class View(object):
self._prev_syntaxes: typing.List[str] = [] self._prev_syntaxes: typing.List[str] = []
self._prev_highlight_commands: typing.List[str] = [] self._prev_highlight_commands: typing.List[str] = []
self._winrestcmd = '' self._winrestcmd = ''
self._has_preview_window = False
self._session_version = '1.0' self._session_version = '1.0'
self._sessions: typing.Dict[str, Session] = {} 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, paths: typing.List[str], def init(self, context: typing.Dict[str, typing.Any]) -> None:
context: typing.Dict[str, typing.Any],
clipboard: Clipboard
) -> None:
self._context = self._init_context(context) self._context = self._init_context(context)
self._bufname = f'[defx] {self._context.buffer_name}-{self._index}' self._bufname = f'[defx] {self._context.buffer_name}-{self._index}'
self._winrestcmd = self._vim.call('winrestcmd') self._winrestcmd = self._vim.call('winrestcmd')
self._prev_wininfo = self._get_wininfo() self._prev_wininfo = self._get_wininfo()
self._prev_bufnr = self._context.prev_bufnr 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 not self._init_defx(paths, clipboard): if self._vim.call('defx#util#has_textprop'):
# Skipped initialize self._has_textprop = True
self._winid = self._vim.call('win_getid') else:
if paths and self._vim.call('bufnr', '%') == self._bufnr: self._ns = self._vim.call('nvim_create_namespace', 'defx')
self._update_defx(paths)
self._init_columns(self._context.columns.split(':')) def init_paths(self, paths: typing.List[typing.List[str]],
self.redraw(True) 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, def do_action(self, action_name: str,
action_args: typing.List[str], action_args: typing.List[str],
@ -93,7 +135,19 @@ class View(object):
def print_msg(self, expr: typing.Any) -> None: def print_msg(self, expr: typing.Any) -> None:
self._vim.call('defx#util#print_message', expr) 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: def quit(self) -> None:
# Close preview window
self.close_preview()
winnr = self._vim.call('bufwinnr', self._bufnr) winnr = self._vim.call('bufwinnr', self._bufnr)
if winnr < 0: if winnr < 0:
return return
@ -120,6 +174,8 @@ class View(object):
if self._get_wininfo() and self._get_wininfo() == self._prev_wininfo: if self._get_wininfo() and self._get_wininfo() == self._prev_wininfo:
self._vim.command(self._winrestcmd) self._vim.command(self._winrestcmd)
self.restore_previous_buffer()
def redraw(self, is_force: bool = False) -> None: def redraw(self, is_force: bool = False) -> None:
""" """
Redraw defx buffer. Redraw defx buffer.
@ -138,10 +194,14 @@ class View(object):
for column in self._columns: for column in self._columns:
column.on_redraw(self, self._context) column.on_redraw(self, self._context)
lines = [ lines = []
self._get_columns_text(self._context, x) columns_highlights = []
for x in self._candidates 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 self._buffer.options['modifiable'] = True
@ -165,6 +225,11 @@ class View(object):
if is_force: if is_force:
self._init_column_syntax() 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: if self._context.profile:
error(self._vim, f'redraw time = {time.time() - start}') error(self._vim, f'redraw time = {time.time() - start}')
@ -193,7 +258,8 @@ class View(object):
return pos return pos
return -1 return -1
def cd(self, defx: Defx, path: str, cursor: int) -> None: def cd(self, defx: Defx, source_name: str,
path: str, cursor: int) -> None:
history = defx._cursor_history history = defx._cursor_history
# Save previous cursor position # Save previous cursor position
@ -202,9 +268,15 @@ class View(object):
history[defx._cwd] = candidate['action__path'] history[defx._cwd] = candidate['action__path']
global_histories = self._vim.vars['defx#_histories'] global_histories = self._vim.vars['defx#_histories']
global_histories.append(defx._cwd) global_histories.append([defx._source.name, defx._cwd])
self._vim.vars['defx#_histories'] = global_histories 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) defx.cd(path)
self.redraw(True) self.redraw(True)
@ -227,6 +299,21 @@ class View(object):
self._vim.call('cursor', [pos + 1, 1]) self._vim.call('cursor', [pos + 1, 1])
return True 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: def update_candidates(self) -> None:
# Update opened/selected state # Update opened/selected state
for defx in self._defxs: for defx in self._defxs:
@ -265,6 +352,8 @@ class View(object):
if (enable_nested and len(children) == 1 if (enable_nested and len(children) == 1
and children[0]['is_directory']): and children[0]['is_directory']):
# Merge child. # Merge child.
defx._nested_candidates.add(str(target['action__path']))
target['action__path'] = children[0]['action__path'] target['action__path'] = children[0]['action__path']
target['word'] += children[0]['word'] target['word'] += children[0]['word']
target['is_opened_tree'] = False target['is_opened_tree'] = False
@ -289,17 +378,38 @@ class View(object):
target['is_opened_tree'] = False target['is_opened_tree'] = False
defx = self._defxs[index]
self._remove_nested_path(defx, target['action__path'])
start = pos + 1 start = pos + 1
base_level = target['level'] base_level = target['level']
end = start end = start
for candidate in self._candidates[start:]: for candidate in self._candidates[start:]:
if candidate['level'] <= base_level: if candidate['level'] <= base_level:
break break
self._remove_nested_path(defx, candidate['action__path'])
end += 1 end += 1
self._candidates = (self._candidates[: start] + self._candidates = (self._candidates[: start] +
self._candidates[end:]) 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( def _init_context(
self, context: typing.Dict[str, typing.Any]) -> Context: self, context: typing.Dict[str, typing.Any]) -> Context:
# Convert to int # Convert to int
@ -309,7 +419,9 @@ class View(object):
return Context(**context) return Context(**context)
def _resize_window(self) -> None: def _init_window(self) -> None:
self._winid = self._vim.call('win_getid')
window_options = self._vim.current.window.options window_options = self._vim.current.window.options
if (self._context.split == 'vertical' if (self._context.split == 'vertical'
and self._context.winwidth > 0): and self._context.winwidth > 0):
@ -331,30 +443,22 @@ class View(object):
self.update_candidates() self.update_candidates()
self.redraw() self.redraw()
def _init_defx(self, def _init_defx(self, clipboard: Clipboard) -> bool:
paths: typing.List[str],
clipboard: Clipboard) -> bool:
if not self._switch_buffer(): if not self._switch_buffer():
return False return False
self._buffer = self._vim.current.buffer self._buffer = self._vim.current.buffer
self._bufnr = self._buffer.number self._bufnr = self._buffer.number
self._winid = self._vim.call('win_getid')
if not paths:
paths = [self._vim.call('getcwd')]
self._buffer.vars['defx'] = { self._buffer.vars['defx'] = {
'context': self._context._asdict(), 'context': self._context._asdict(),
'paths': paths, 'paths': [],
} }
# Note: Have to use setlocal instead of "current.window.options" # Note: Have to use setlocal instead of "current.window.options"
# "current.window.options" changes global value instead of local in # "current.window.options" changes global value instead of local in
# neovim. # neovim.
self._vim.command('setlocal colorcolumn=') self._vim.command('setlocal colorcolumn=')
self._vim.command('setlocal conceallevel=2')
self._vim.command('setlocal concealcursor=nc')
self._vim.command('setlocal nocursorcolumn') self._vim.command('setlocal nocursorcolumn')
self._vim.command('setlocal nofoldenable') self._vim.command('setlocal nofoldenable')
self._vim.command('setlocal foldcolumn=0') self._vim.command('setlocal foldcolumn=0')
@ -367,7 +471,7 @@ class View(object):
if self._context.split == 'floating': if self._context.split == 'floating':
self._vim.command('setlocal nocursorline') self._vim.command('setlocal nocursorline')
self._resize_window() self._init_window()
buffer_options = self._buffer.options buffer_options = self._buffer.options
if not self._context.listed: if not self._context.listed:
@ -397,22 +501,10 @@ class View(object):
self._candidates = [] self._candidates = []
self._clipboard = clipboard self._clipboard = clipboard
self._defxs = [] self._defxs = []
self._update_defx(paths)
self._init_all_columns() self._init_all_columns()
self._init_columns(self._context.columns.split(':')) 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, path] in enumerate(paths):
self._check_session(index, path)
for defx in self._defxs:
self._init_cursor(defx)
self._vim.vars['defx#_drives'] = self._context.drives self._vim.vars['defx#_drives'] = self._context.drives
return True return True
@ -421,13 +513,18 @@ class View(object):
if self._context.split == 'tab': if self._context.split == 'tab':
self._vim.command('tabnew') self._vim.command('tabnew')
if self._context.close:
self.quit()
return False
winnr = self._vim.call('bufwinnr', self._bufnr) winnr = self._vim.call('bufwinnr', self._bufnr)
if winnr > 0: if winnr > 0:
self._vim.command(f'{winnr}wincmd w') self._vim.command(f'{winnr}wincmd w')
if self._context.toggle: if self._context.toggle:
self.quit() self.quit()
else: else:
self._resize_window() self._winid = self._vim.call('win_getid')
self._init_window()
return False return False
if (self._vim.current.buffer.options['modified'] and if (self._vim.current.buffer.options['modified'] and
@ -462,7 +559,7 @@ class View(object):
) )
) )
if self._context.resume: if self._context.resume:
self._resize_window() self._init_window()
return False return False
elif self._vim.call('exists', 'bufadd'): elif self._vim.call('exists', 'bufadd'):
bufnr = self._vim.call('bufadd', self._bufname) bufnr = self._vim.call('bufadd', self._bufname)
@ -519,6 +616,7 @@ class View(object):
start = 1 start = 1
for [index, column] in enumerate(self._columns): for [index, column] in enumerate(self._columns):
column.syntax_name = f'Defx_{column.name}_{index}' column.syntax_name = f'Defx_{column.name}_{index}'
column.highlight_name = f'Defx_{column.name}'
if within_variable and not column.is_stop_variable: if within_variable and not column.is_stop_variable:
within_variable_columns.append(column) within_variable_columns.append(column)
@ -563,29 +661,31 @@ class View(object):
commands.append( commands.append(
'silent! syntax clear ' + syntax) 'silent! syntax clear ' + syntax)
if self._proptypes:
self._clear_prop_types()
self._prev_syntaxes = [] self._prev_syntaxes = []
for column in self._columns: for column in self._columns:
source_highlights = column.highlight_commands() source_highlights = column.highlight_commands()
if source_highlights: if not source_highlights:
if (not column.is_within_variable and continue
column.start > 0 and column.end > 0):
commands.append(
'syntax region ' + column.syntax_name +
r' start=/\%' + str(column.start) + r'v/ end=/\%' +
str(column.end) + 'v/ keepend oneline')
self._prev_syntaxes += [column.syntax_name]
commands += source_highlights commands += source_highlights
self._prev_syntaxes += column.syntaxes() self._prev_syntaxes += column.syntaxes()
syntax_list = commands + [self._vim.call('execute', 'syntax list')] syntax_list = commands + [
self._vim.call('execute', 'syntax list'),
self._vim.call('execute', 'highlight'),
]
if syntax_list == self._prev_highlight_commands: if syntax_list == self._prev_highlight_commands:
# Skip highlights # Skip highlights
return return
self._execute_commands(commands) self._execute_commands(commands)
self._prev_highlight_commands = commands + [ self._prev_highlight_commands = commands + [
self._vim.call('execute', 'syntax list')] self._vim.call('execute', 'syntax list'),
self._vim.call('execute', 'highlight'),
]
def _execute_commands(self, commands: typing.List[str]) -> None: def _execute_commands(self, commands: typing.List[str]) -> None:
# Note: If commands are too huge, vim.command() will fail. # Note: If commands are too huge, vim.command() will fail.
@ -608,27 +708,43 @@ class View(object):
candidate['_defx_index'] = defx._index candidate['_defx_index'] = defx._index
self._candidates += candidates self._candidates += candidates
def _get_columns_text(self, context: Context, def _get_columns_text(self, context: Context, candidate: Candidate
candidate: typing.Dict[str, typing.Any]) -> str: ) -> typing.Tuple[str, Highlights]:
texts: typing.List[str] = [] texts: typing.List[str] = []
variable_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: for column in self._columns:
column.start = start
if column.is_stop_variable: if column.is_stop_variable:
if variable_texts: if variable_texts:
variable_texts.append('') variable_texts.append('')
text = column.get_with_variable_text( (text, highlights) = column.get_with_variable_text(
context, ' '.join(variable_texts), candidate) context, ' '.join(variable_texts), candidate)
texts.append(text) texts.append(text)
ret_highlights += highlights
variable_texts = [] variable_texts = []
else: else:
text = column.get(context, candidate) 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 column.is_start_variable or column.is_within_variable:
if text: if text:
variable_texts.append(text) variable_texts.append(text)
else: else:
texts.append(text) texts.append(text)
return ' '.join(texts) 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: def _update_paths(self, index: int, path: str) -> None:
var_defx = self._buffer.vars['defx'] var_defx = self._buffer.vars['defx']
@ -662,17 +778,57 @@ class View(object):
return result return result
def _update_defx(self, paths: typing.List[str]) -> None: def _update_defx_paths(self,
paths: typing.List[typing.List[str]]) -> None:
self._defxs = self._defxs[:len(paths)] self._defxs = self._defxs[:len(paths)]
for [index, path] in enumerate(paths): for [index, [source_name, path]] in enumerate(paths):
if index >= len(self._defxs): if index >= len(self._defxs):
self._defxs.append( self._defxs.append(
Defx(self._vim, self._context, path, index)) Defx(self._vim, self._context, source_name, path, index))
else: else:
self.cd(self._defxs[index], path, self._context.cursor) defx = self._defxs[index]
self.cd(defx, defx._source.name, path, self._context.cursor)
self._update_paths(index, path) self._update_paths(index, path)
def _check_bufnr(self, bufnr: int) -> bool: def _check_bufnr(self, bufnr: int) -> bool:
return (bool(self._vim.call('bufexists', bufnr)) and return (bool(self._vim.call('bufexists', bufnr)) and
bufnr != self._vim.call('bufnr', '%')) 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')

View File

@ -29,7 +29,8 @@ class Source(Base):
def gather_candidates(self, context: UserContext) -> Candidates: def gather_candidates(self, context: UserContext) -> Candidates:
return [{ return [{
'word': x, 'word': x,
'abbr': x + '/', 'abbr': f'{source_name}:{x}/',
'action__command': f"call defx#call_action('cd', ['{x}'])", 'action__command': ('call defx#call_action' +
f"('cd', ['{source_name}', '{x}'])"),
'action__path': x, 'action__path': x,
} for x in self._histories] } for [source_name, x] in self._histories]

View File

@ -147,6 +147,8 @@ function! s:defx_init()
nnoremap <silent><buffer><expr> yy defx#do_action('call', 'DefxYarkPath') nnoremap <silent><buffer><expr> yy defx#do_action('call', 'DefxYarkPath')
nnoremap <silent><buffer><expr> . nnoremap <silent><buffer><expr> .
\ defx#do_action('toggle_ignored_files') \ defx#do_action('toggle_ignored_files')
nnoremap <silent><buffer><expr> <C-f>
\ defx#do_action('change_filtered_files')
nnoremap <silent><buffer><expr> ~ nnoremap <silent><buffer><expr> ~
\ defx#do_action('cd') \ defx#do_action('cd')
nnoremap <silent><buffer><expr> j nnoremap <silent><buffer><expr> j