├── README.md ├── .github └── workflows │ └── jsr.yml ├── deno.jsonc ├── denops └── @ddu-kinds │ └── file │ ├── deno.json │ └── main.ts ├── LICENSE ├── autoload └── ddu │ └── kind │ ├── file.vim │ └── file │ └── exrename.vim └── doc └── ddu-kind-file.txt /README.md: -------------------------------------------------------------------------------- 1 | # ddu-kind-file 2 | 3 | File kind for ddu.vim 4 | 5 | This kind implements file operations. 6 | 7 | ## Required 8 | 9 | ### denops.vim 10 | 11 | https://github.com/vim-denops/denops.vim 12 | 13 | ### ddu.vim 14 | 15 | https://github.com/Shougo/ddu.vim 16 | 17 | ## Configuration 18 | 19 | ```vim 20 | call ddu#custom#patch_global(#{ 21 | \ kindOptions: #{ 22 | \ file: #{ 23 | \ defaultAction: 'open', 24 | \ }, 25 | \ } 26 | \ }) 27 | ``` 28 | -------------------------------------------------------------------------------- /.github/workflows/jsr.yml: -------------------------------------------------------------------------------- 1 | name: jsr 2 | 3 | env: 4 | DENO_VERSION: 2.x 5 | 6 | on: 7 | push: 8 | tags: 9 | - "v*" 10 | 11 | permissions: 12 | contents: read 13 | id-token: write 14 | 15 | jobs: 16 | publish: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - uses: denoland/setup-deno@v2 23 | with: 24 | deno-version: ${{ env.DENO_VERSION }} 25 | - name: Publish 26 | run: | 27 | deno run -A jsr:@david/publish-on-tag@0.2.0 28 | 29 | -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "lock": false, 3 | "tasks": { 4 | "cache": "deno install --reload", 5 | "check": "deno check denops/**/*.ts", 6 | "lint": "deno lint denops", 7 | "lint-fix": "deno lint --fix denops", 8 | "fmt": "deno fmt denops", 9 | "test": "deno test -A --doc --parallel --shuffle denops/**/*.ts", 10 | "test:publish": "deno publish --dry-run --allow-dirty --set-version 0.0.0", 11 | "update": "deno outdated --recursive", 12 | "upgrade": "deno outdated --recursive --update" 13 | }, 14 | "workspace": [ 15 | "./denops/@ddu-kinds/file" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /denops/@ddu-kinds/file/deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@shougo/ddu-kind-file", 3 | "exports": { 4 | ".": "./main.ts" 5 | }, 6 | "publish": { 7 | "include": [ 8 | "**/*.ts" 9 | ] 10 | }, 11 | "imports": { 12 | "@core/unknownutil": "jsr:@core/unknownutil@~4.3.0", 13 | "@denops/std": "jsr:@denops/std@~8.0.0", 14 | "@shougo/ddc-vim": "jsr:@shougo/ddc-vim@~10.0.0", 15 | "@shougo/ddu-vim": "jsr:@shougo/ddu-vim@~11.0.0", 16 | "@std/fs": "jsr:@std/fs@~1.0.0", 17 | "@std/path": "jsr:@std/path@~1.1.0", 18 | "@std/streams": "jsr:@std/streams@~1.0.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT license 2 | 3 | Copyright (c) Shougo Matsushita 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included 14 | in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /autoload/ddu/kind/file.vim: -------------------------------------------------------------------------------- 1 | let s:is_windows = has('win32') || has('win64') 2 | 3 | function! ddu#kind#file#open(filename, method) abort 4 | let filename = a:filename->fnamemodify(':p') 5 | 6 | const method = a:method ==# '' ? s:detect_method() : a:method 7 | 8 | if method ==# 'nvim-open' 9 | " Use vim.ui.open instead 10 | 11 | " NOTE: vim.ui.open seems broken by 12 | " "Vim(call):E5101: Cannot convert given lua type (code: 0)" 13 | silent! call v:lua.vim.ui.open(filename) 14 | 15 | return 16 | elseif method ==# 'windows-rundll32' 17 | " NOTE: 18 | " # and % required to be escaped (:help cmdline-special) 19 | silent execute printf( 20 | \ '!start rundll32 url.dll,FileProtocolHandler %s', 21 | \ filename->escape('#%'), 22 | \ ) 23 | return 24 | elseif method ==# 'kioclient' 25 | let command = 'kioclient exec' 26 | elseif method->executable() 27 | let command = method 28 | else 29 | if is_wsl && 'cmd.exe'->executable() 30 | " WSL and not installed any open commands 31 | 32 | " Open the same way as Windows. 33 | " I don't know why, but the method using execute requires redraw 34 | " after execution in vim. 35 | call system(printf('cmd.exe /c start rundll32 %s %s', 36 | \ 'url.dll,FileProtocolHandler', 37 | \ filename->escape('#%')), 38 | \ ) 39 | return 40 | endif 41 | 42 | " Give up. 43 | throw 'Not supported.' 44 | endif 45 | 46 | call system(printf('%s %s &', command, filename->shellescape())) 47 | endfunction 48 | 49 | function! ddu#kind#file#cwd_input(cwd, prompt, text, completion) abort 50 | redraw 51 | 52 | let prev = getcwd() 53 | try 54 | if a:cwd !=# '' 55 | call chdir(a:cwd) 56 | endif 57 | return input(a:prompt, a:text, a:completion) 58 | catch /^Vim:Interrupt/ 59 | finally 60 | call chdir(prev) 61 | endtry 62 | 63 | return '' 64 | endfunction 65 | 66 | function! ddu#kind#file#confirm(msg, choices, default) abort 67 | try 68 | return confirm(a:msg, a:choices, a:default) 69 | catch 70 | " ignore the errors 71 | endtry 72 | 73 | return a:default 74 | endfunction 75 | 76 | function! ddu#kind#file#print(string, ...) abort 77 | let name = a:0 ? a:1 : 'ddu-kind-file' 78 | echomsg printf('[%s] %s', name, 79 | \ a:string->type() ==# v:t_string ? a:string : a:string->string()) 80 | endfunction 81 | 82 | function! ddu#kind#file#buffer_rename(bufnr, new_filename) abort 83 | if a:bufnr < 0 || !(a:bufnr->bufloaded()) 84 | return 85 | endif 86 | 87 | let hidden = &hidden 88 | 89 | set hidden 90 | let bufnr_save = '%'->bufnr() 91 | noautocmd silent! execute 'buffer' a:bufnr 92 | silent! execute (&l:buftype ==# '' ? 'saveas!' : 'file') 93 | \ a:new_filename->fnameescape() 94 | if &l:buftype ==# '' 95 | " Remove old buffer. 96 | silent! bdelete! # 97 | endif 98 | 99 | noautocmd silent execute 'buffer' bufnr_save 100 | let &hidden = hidden 101 | endfunction 102 | 103 | function! ddu#kind#file#buffer_delete(bufnr) abort 104 | if a:bufnr < 0 105 | return 106 | endif 107 | 108 | const winid = a:bufnr->win_findbuf()->get(0, -1) 109 | if winid > 0 110 | const winid_save = win_getid() 111 | call win_gotoid(winid) 112 | 113 | noautocmd silent enew 114 | execute 'silent! bdelete!' a:bufnr 115 | 116 | call win_gotoid(winid_save) 117 | else 118 | execute 'silent! bdelete!' a:bufnr 119 | endif 120 | endfunction 121 | 122 | function! ddu#kind#file#bufnr(filename) abort 123 | " NOTE: bufnr() may be wrong. It returns submatched buffer number. 124 | return a:filename->bufexists() ? a:filename->bufnr() : -1 125 | endfunction 126 | 127 | function! s:check_wsl() abort 128 | if has('nvim') 129 | return has('wsl') 130 | endif 131 | if has('unix') && 'uname'->executable() 132 | return 'uname -r'->system()->match("\\cMicrosoft") >= 0 133 | endif 134 | return v:false 135 | endfunction 136 | 137 | function! s:detect_method() abort 138 | if has('nvim-0.10') 139 | " Use vim.ui.open instead 140 | return 'nvim-open' 141 | endif 142 | 143 | let is_cygwin = has('win32unix') 144 | let is_mac = !s:is_windows && !is_cygwin 145 | \ && (has('mac') || has('macunix') || has('gui_macvim') || 146 | \ (!'/proc'->isdirectory() && 'sw_vers'->executable())) 147 | let is_wsl = s:check_wsl() 148 | 149 | if s:is_windows 150 | return 'windows-rundll32' 151 | endif 152 | 153 | if is_cygwin 154 | " Cygwin. 155 | return 'cygstart' 156 | elseif is_mac && executable('open') 157 | " Mac OS. 158 | return 'open' 159 | elseif is_wsl && executable('wslview') 160 | return 'wslview' 161 | elseif 'xdg-open'->executable() 162 | return 'xdg-open' 163 | elseif '$KDE_FULL_SESSION'->exists() && $KDE_FULL_SESSION ==# 'true' 164 | return 'kioclient' 165 | elseif exists('$GNOME_DESKTOP_SESSION_ID') 166 | return 'gnome-open' 167 | elseif 'exo-open'->executable() 168 | return 'exo-open' 169 | endif 170 | 171 | return '' 172 | endfunction 173 | -------------------------------------------------------------------------------- /autoload/ddu/kind/file/exrename.vim: -------------------------------------------------------------------------------- 1 | "============================================================================= 2 | " FILE: exrename.vim 3 | " AUTHOR: Shougo Matsushita 4 | " EDITOR: Alisue 5 | " License: MIT license 6 | "============================================================================= 7 | 8 | let s:PREFIX = has('win32') ? '[exrename]' : '*exrename*' 9 | 10 | function! ddu#kind#file#exrename#create_buffer(items, ...) abort 11 | let options = extend(#{ 12 | \ cwd: getcwd(), 13 | \ bufnr: '%'->bufnr(), 14 | \ buffer_name: '', 15 | \ post_rename_callback: v:null, 16 | \ }, a:000->get(0, {})) 17 | if options.cwd !~# '/$' 18 | " current working directory MUST end with a trailing slash 19 | let options.cwd ..= '/' 20 | endif 21 | let options.buffer_name = s:PREFIX 22 | if options.buffer_name !=# '' 23 | let options.buffer_name ..= ' - ' .. options.buffer_name 24 | endif 25 | 26 | let winid = win_getid() 27 | 28 | let bufnr = options.buffer_name->bufadd() 29 | call bufload(bufnr) 30 | execute 'vertical sbuffer' bufnr 31 | 32 | setlocal buftype=acwrite 33 | setlocal noswapfile 34 | setfiletype ddu_exrename 35 | 36 | syntax match dduExrenameModified '^.*$' 37 | 38 | highlight def link dduExrenameModified Todo 39 | highlight def link dduExrenameOriginal Normal 40 | 41 | let b:exrename = options 42 | 43 | call chdir(b:exrename.cwd) 44 | 45 | nnoremap q call exit('%'->bufnr()) 46 | augroup ddu-exrename 47 | autocmd! * 48 | autocmd BufHidden call s:exit(''->expand()) 49 | autocmd BufWriteCmd call s:do_rename() 50 | autocmd CursorMoved,CursorMovedI call s:check_lines() 51 | augroup END 52 | 53 | " Clean up the screen. 54 | silent % delete _ 55 | silent! syntax clear dduExrenameOriginal 56 | 57 | " Validate items and register 58 | let unique_filenames = {} 59 | let b:exrename.items = [] 60 | let b:exrename.filenames = [] 61 | let cnt = 1 62 | for item in a:items 63 | " Make sure that the 'action__path' is absolute path 64 | if !s:is_absolute(item.action__path) 65 | let item.action__path = b:exrename.cwd .. item.action__path 66 | endif 67 | 68 | " Make sure that the 'action__path' exists 69 | if !(item.action__path->filewritable()) 70 | \ && !(item.action__path->isdirectory()) 71 | redraw 72 | call ddu#util#print_error( 73 | \ item.action__path .. ' does not exist. Skip.') 74 | continue 75 | endif 76 | 77 | " Make sure that the 'action__path' is unique 78 | if unique_filenames->has_key(item.action__path) 79 | redraw 80 | call ddu#util#print_error( 81 | \ item.action__path .. ' is duplicated. Skip.') 82 | continue 83 | endif 84 | 85 | " Create filename 86 | let filename = item.action__path 87 | if filename->stridx(b:exrename.cwd) == 0 88 | let filename = filename[b:exrename.cwd->len() :] 89 | endif 90 | " directory should end with a trailing slash (to distinguish easily) 91 | if item.action__path->isdirectory() 92 | let filename ..= '/' 93 | endif 94 | 95 | let pattern = s:escape_pattern(filename)->escape('/') 96 | execute 'syntax match dduExrenameOriginal' 97 | \ '/' .. printf('^\%%%dl%s$', cnt, pattern) .. '/' 98 | 99 | " Register 100 | let unique_filenames[item.action__path] = 1 101 | call add(b:exrename.items, item) 102 | call add(b:exrename.filenames, filename) 103 | 104 | let cnt += 1 105 | endfor 106 | 107 | let b:exrename.unique_filenames = unique_filenames 108 | let b:exrename.prev_winid = winid 109 | 110 | " write filenames 111 | let [undolevels, &l:undolevels] = [&l:undolevels, -1] 112 | try 113 | call setline(1, b:exrename.filenames) 114 | finally 115 | let &l:undolevels = undolevels 116 | endtry 117 | setlocal nomodified 118 | 119 | " Move to the UI window 120 | call win_gotoid(winid) 121 | endfunction 122 | 123 | function! s:escape_pattern(str) abort 124 | return a:str->escape('~"\.^$[]*') 125 | endfunction 126 | 127 | function! s:is_absolute(path) abort 128 | return a:path =~# '^\%(\a\a\+:\)\|^\%(\a:\|/\)' 129 | endfunction 130 | 131 | function! s:do_rename() abort 132 | if '$'->line() != b:exrename.filenames->len() 133 | call ddu#util#print_error('Invalid rename buffer!') 134 | return 135 | endif 136 | 137 | " Rename files. 138 | let linenr = 1 139 | let max = '$'->line() 140 | while linenr <= max 141 | let filename = b:exrename.filenames[linenr - 1] 142 | 143 | redraw 144 | echo printf('(%' .. len(max) .. 'd/%d): %s -> %s', 145 | \ linenr, max, filename, linenr->getline()) 146 | 147 | if filename ==# linenr->getline() 148 | let linenr += 1 149 | continue 150 | endif 151 | 152 | let old_file = b:exrename.items[linenr - 1].action__path 153 | let new_file = linenr->getline()->expand() 154 | if !s:is_absolute(new_file) 155 | " Convert to absolute path 156 | let new_file = b:exrename.cwd . new_file 157 | endif 158 | 159 | if new_file->filereadable() || new_file->isdirectory() 160 | " new_file is already exists. 161 | redraw 162 | call ddu#util#print_error( 163 | \ new_file .. ' is already exists. Skip.') 164 | 165 | let linenr += 1 166 | continue 167 | endif 168 | 169 | " Create the parent directory. 170 | call mkdir(new_file->fnamemodify(':h'), 'p') 171 | 172 | if rename(old_file, new_file) 173 | " Rename error 174 | redraw 175 | call ddu#util#print_error( 176 | \ new_file .. ' is rename error. Skip.') 177 | 178 | let linenr += 1 179 | continue 180 | endif 181 | 182 | call ddu#kind#file#buffer_rename(bufnr(old_file), new_file) 183 | 184 | " update b:exrename 185 | let b:exrename.filenames[linenr - 1] = linenr->getline() 186 | let b:exrename.items[linenr - 1].action__path = new_file 187 | 188 | let linenr += 1 189 | endwhile 190 | 191 | redraw 192 | echo 'Rename done!' 193 | 194 | setlocal nomodified 195 | 196 | if b:exrename.post_rename_callback != v:null 197 | call b:exrename.post_rename_callback(b:exrename) 198 | endif 199 | endfunction 200 | 201 | function! s:exit(bufnr) abort 202 | if !(a:bufnr->bufexists()) 203 | return 204 | endif 205 | 206 | let exrename = a:bufnr->getbufvar('exrename', {}) 207 | 208 | " Switch buffer. 209 | if '#'->winnr() > 0 && '#'->winnr() !=# winnr() 210 | close 211 | else 212 | call s:custom_alternate_buffer() 213 | endif 214 | silent execute 'bdelete!' a:bufnr 215 | 216 | call win_gotoid(exrename.prev_winid) 217 | call ddu#redraw(exrename.name, #{ method: 'refreshItems' }) 218 | endfunction 219 | 220 | function! s:check_lines() abort 221 | if !('b:exrename'->exists()) 222 | return 223 | endif 224 | 225 | if '$'->line() != b:exrename.filenames->len() 226 | call ddu#util#print_error('Invalid rename buffer!') 227 | return 228 | endif 229 | endfunction 230 | 231 | function! s:custom_alternate_buffer() abort 232 | if '%'->bufnr() != '#'->bufnr() && '#'->bufnr()->buflisted() 233 | buffer # 234 | endif 235 | 236 | let cnt = 0 237 | let pos = 1 238 | let current = 0 239 | while pos <= '$'->bufnr() 240 | if pos->buflisted() 241 | if pos == '%'->bufnr() 242 | let current = cnt 243 | endif 244 | 245 | let cnt += 1 246 | endif 247 | 248 | let pos += 1 249 | endwhile 250 | 251 | if current > cnt / 2 252 | bprevious 253 | else 254 | bnext 255 | endif 256 | endfunction 257 | -------------------------------------------------------------------------------- /doc/ddu-kind-file.txt: -------------------------------------------------------------------------------- 1 | *ddu-kind-file.txt* File kind for ddu.vim 2 | 3 | Author: Shougo 4 | License: MIT license 5 | 6 | CONTENTS *ddu-kind-file-contents* 7 | 8 | Introduction |ddu-kind-file-introduction| 9 | Install |ddu-kind-file-install| 10 | Examples |ddu-kind-file-examples| 11 | Actions |ddu-kind-file-actions| 12 | Preview params |ddu-kind-file-preview-params| 13 | Params |ddu-kind-file-params| 14 | Compatibility |ddu-kind-file-compatibility| 15 | 16 | 17 | ============================================================================== 18 | INTRODUCTION *ddu-kind-file-introduction* 19 | 20 | This kind implements file operations. 21 | 22 | 23 | ============================================================================== 24 | INSTALL *ddu-kind-file-install* 25 | 26 | Please install both "ddu.vim" and "denops.vim". 27 | 28 | https://github.com/Shougo/ddu.vim 29 | https://github.com/vim-denops/denops.vim 30 | 31 | 32 | ============================================================================== 33 | EXAMPLES *ddu-kind-file-examples* 34 | >vim 35 | call ddu#custom#patch_global(#{ 36 | \ kindOptions: #{ 37 | \ file: #{ 38 | \ defaultAction: 'open', 39 | \ }, 40 | \ } 41 | \ }) 42 | < 43 | 44 | ============================================================================== 45 | ACTIONS *ddu-kind-file-actions* 46 | 47 | *ddu-kind-file-action-append* 48 | append 49 | Paste the path like |p|. 50 | 51 | *ddu-kind-file-action-cd* 52 | cd 53 | Call |chdir()| the directory. 54 | 55 | *ddu-kind-file-action-clipCopy* 56 | clipCopy 57 | Copy files from clipboard register. 58 | It is useful to copy files across Vim/Neovim instances. 59 | NOTE: It cannot support |ddu-kind-file-action-undo| action. 60 | 61 | *ddu-kind-file-action-clipMove* 62 | clipMove 63 | Move files from clipboard register. 64 | It is useful to move files across Vim/Neovim instances. 65 | NOTE: It cannot support |ddu-kind-file-action-undo| action. 66 | 67 | *ddu-kind-file-action-clipYank* 68 | clipYank 69 | Yank the file path to clipboard register. 70 | 71 | *ddu-kind-file-action-copy* 72 | copy 73 | Copy the selected files to ddu clipboard. 74 | 75 | *ddu-kind-file-action-delete* 76 | delete 77 | Delete the file or directory. 78 | NOTE: It removes the deleted buffer. 79 | NOTE: It cannot support |ddu-kind-file-action-undo| action. 80 | 81 | *ddu-kind-file-action-execute* 82 | execute 83 | Execute the file. 84 | 85 | params: 86 | {command}: execute command. 87 | (Default: "edit") 88 | 89 | *ddu-kind-file-action-executeSystem* 90 | executeSystem 91 | Execute the file by system associated command. 92 | 93 | params: 94 | {method}: specify the execute method. 95 | (Default: "") 96 | 97 | "nvim-open": Use |vim.ui.open()|. 98 | "windows-rundll32": Use Windows rundll32. 99 | "cygstart": Use "cygstart" command. 100 | "open": Use "open" command. 101 | "wslview": Use "wslview" command. 102 | "xdg-open": Use "xdg-open" command. 103 | "kioclient": Use "kioclient" command. 104 | "gnome-open": Use "gnome-open" command. 105 | "exo-open": Use "exo-open" command. 106 | "": Detect method automatically. 107 | 108 | *ddu-kind-file-action-feedkeys* 109 | feedkeys 110 | Use |feedkeys()| to insert the path. 111 | It is useful in command line mode. 112 | 113 | *ddu-kind-file-action-insert* 114 | insert 115 | Paste the path like |P|. 116 | 117 | *ddu-kind-file-action-link* 118 | link 119 | Create link the selected files to ddu clipboard. 120 | 121 | params: 122 | {mode}: create link mode. 123 | 124 | "hard": Create hard link. 125 | "relative": Create relative symbolic link from 126 | destination parent. 127 | "absolute": Create absolute symbolic link. 128 | default: Create symbolic link. 129 | 130 | *ddu-kind-file-action-loclist* 131 | loclist 132 | Set the |location-list| and open the |location-list| window. 133 | 134 | *ddu-kind-file-action-move* 135 | move 136 | Move the selected files to ddu clipboard. 137 | 138 | *ddu-kind-file-action-narrow* 139 | narrow 140 | Change |ddu-source-option-path| to the directory. 141 | NOTE: If you select multiple files, it will start "file" 142 | source. 143 | 144 | NOTE: "ddu-source-file" is required. 145 | https://github.com/Shougo/ddu-source-file 146 | 147 | params: 148 | {path}: narrowing path. 149 | If it is "..", it means the parent 150 | directory. 151 | (Default: cursor/selected item's path) 152 | 153 | *ddu-kind-file-action-newDirectory* 154 | newDirectory 155 | Make new directory in expanded directory tree or current 156 | directory. 157 | If the input is comma separated, multiple directories are 158 | created. 159 | 160 | *ddu-kind-file-action-newFile* 161 | newFile 162 | Make new file in expanded directory tree or current directory. 163 | If the input ends with "/", it means new directory. 164 | If the input is comma separated, multiple files are created. 165 | 166 | *ddu-kind-file-action-open* 167 | open 168 | Open the items. 169 | If the item is buffer, switch to the buffer. 170 | If the item is file, open the file. 171 | 172 | params: 173 | {command}: open command. 174 | (Default: "edit") 175 | {maxSize}: max size of preview file. 176 | (Default: 500000) 177 | 178 | paste *ddu-kind-file-action-paste* 179 | Fire the clipboard action in the current directory. 180 | NOTE: It is used after |ddu-kind-file-action-copy| or 181 | |ddu-kind-file-action-move|. 182 | 183 | *ddu-kind-file-action-quickfix* 184 | quickfix 185 | Set the |quickfix| list and open the |quickfix| window. 186 | 187 | rename *ddu-kind-action-rename* 188 | Rename the file/directory under cursor or from selected list. 189 | NOTE: If you select multiple files, it will be buffer-rename 190 | mode. 191 | NOTE: If you select multiple files, it cannot support 192 | |ddu-kind-file-action-undo| action. 193 | 194 | *ddu-kind-file-action-trash* 195 | trash 196 | Move the file or directory to the trash. 197 | It uses |ddu-kind-file-param-trashCommand|. 198 | 199 | NOTE: It removes the deleted buffer. 200 | NOTE: It cannot support |ddu-kind-file-action-undo| action. 201 | If you need undo the action, you need to move the files from 202 | trash manually. 203 | 204 | *ddu-kind-file-action-undo* 205 | undo 206 | Undo the previous action. 207 | NOTE: It can undo only the supported actions. 208 | 209 | *ddu-kind-file-action-yank* 210 | yank 211 | Yank the file path to unnamed register. 212 | 213 | ============================================================================== 214 | PREVIEW PARAMS *ddu-kind-file-preview-params* 215 | 216 | *ddu-kind-file-preview-param-maxSize* 217 | maxSize (number) 218 | Max size of preview file. 219 | 220 | Default: 500000 221 | 222 | *ddu-kind-file-preview-param-previewCmds* 223 | previewCmds (string[]) 224 | External commands to preview the file. 225 | If it is not specified, normal buffer is used. 226 | You can use the format like the following. 227 | Symbol Result ~ 228 | -------- ------ 229 | %% % 230 | %s Path to preview 231 | %l Line in file 232 | %h Height of preview window 233 | %e End line of preview 234 | %b Start line of preview 235 | 236 | Example: 237 | Preview with "bat" (https://github.com/sharkdp/bat) > 238 | ["bat", "-n", "%s", "-r", "%b:%e", "--highlight-line", "%l"] 239 | < 240 | Preview with "less" > 241 | ["less", "+%b", "%s"] 242 | < 243 | 244 | ============================================================================== 245 | PARAMS *ddu-kind-file-params* 246 | 247 | *ddu-kind-file-param-trashCommand* 248 | trashCommand (string[]) 249 | Trash command. 250 | 251 | Example: 252 | Trash with "gtrash"(https://github.com/umlx5h/gtrash) > 253 | ["gtrash", "put"] 254 | < 255 | Default: ["gio", "trash"] 256 | 257 | 258 | ============================================================================== 259 | COMPATIBILITY *ddu-kind-file-compatibility* 260 | 261 | 2023.09.14 262 | * Remove url support. Please use "ddu-kind-url" instead. 263 | 264 | 265 | ============================================================================== 266 | vim:tw=78:ts=8:ft=help:norl:noet:fen:noet: 267 | -------------------------------------------------------------------------------- /denops/@ddu-kinds/file/main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ActionFlags, 3 | type ActionHistory, 4 | type Actions, 5 | type BaseParams, 6 | type Clipboard, 7 | type Context, 8 | type DduItem, 9 | type DduOptions, 10 | type PreviewContext, 11 | type Previewer, 12 | type SourceOptions, 13 | } from "@shougo/ddu-vim/types"; 14 | import { BaseKind } from "@shougo/ddu-vim/kind"; 15 | import { 16 | printError, 17 | treePath2Filename, 18 | } from "@shougo/ddu-vim/utils"; 19 | 20 | import type { Denops } from "@denops/std"; 21 | import * as fn from "@denops/std/function"; 22 | import * as vars from "@denops/std/variable"; 23 | 24 | import { basename } from "@std/path/basename"; 25 | import { dirname } from "@std/path/dirname"; 26 | import { isAbsolute } from "@std/path/is-absolute"; 27 | import { join } from "@std/path/join"; 28 | import { normalize } from "@std/path/normalize"; 29 | import { relative } from "@std/path/relative"; 30 | import { copy } from "@std/fs/copy"; 31 | import { ensureDir } from "@std/fs/ensure-dir"; 32 | import { ensureFile } from "@std/fs/ensure-file"; 33 | import { move } from "@std/fs/move"; 34 | import { ByteSliceStream } from "@std/streams/byte-slice-stream"; 35 | import { toArrayBuffer } from "@std/streams/to-array-buffer"; 36 | import { TextLineStream } from "@std/streams/text-line-stream"; 37 | import { ensure as unknownEnsure } from "@core/unknownutil/ensure"; 38 | import { is } from "@core/unknownutil/is"; 39 | 40 | export type ActionData = { 41 | bufNr?: number; 42 | col?: number; 43 | isDirectory?: boolean; 44 | isLink?: boolean; 45 | lineNr?: number; 46 | path?: string; 47 | text?: string; 48 | }; 49 | 50 | export const FileActions: Actions = { 51 | append: { 52 | description: "Paste the path like |p|.", 53 | callback: async ( 54 | args: { denops: Denops; context: Context; items: DduItem[] }, 55 | ) => { 56 | for (const item of args.items) { 57 | await paste(args.denops, item, "p"); 58 | } 59 | return ActionFlags.None; 60 | }, 61 | }, 62 | cd: { 63 | description: "Call |chdir()| the directory.", 64 | callback: async (args: { denops: Denops; items: DduItem[] }) => { 65 | for (const item of args.items) { 66 | const dir = await getDirectory(item); 67 | if (dir === "") { 68 | await printError( 69 | args.denops, 70 | `${dir} is not found.`, 71 | ); 72 | 73 | continue; 74 | } 75 | 76 | await args.denops.call( 77 | "chdir", 78 | dir, 79 | ); 80 | } 81 | 82 | return ActionFlags.None; 83 | }, 84 | }, 85 | clipCopy: { 86 | description: "Copy files from clipboard register.", 87 | callback: async ( 88 | args: { 89 | denops: Denops; 90 | items: DduItem[]; 91 | sourceOptions: SourceOptions; 92 | clipboard: Clipboard; 93 | actionHistory: ActionHistory; 94 | }, 95 | ) => { 96 | const cwd = await getTargetDirectory( 97 | args.denops, 98 | treePath2Filename(args.sourceOptions.path), 99 | args.items, 100 | ); 101 | 102 | const clipboard = await fn.getreg(args.denops, "+") as string; 103 | let searchPath = ""; 104 | for (const path of clipboard.split("\n")) { 105 | const dest = await checkOverwrite( 106 | args.denops, 107 | path, 108 | join(cwd, basename(path)), 109 | ); 110 | if (dest === "") { 111 | continue; 112 | } 113 | 114 | await safeAction("copy", path, dest); 115 | 116 | searchPath = dest; 117 | } 118 | 119 | if (searchPath === "") { 120 | return ActionFlags.Persist; 121 | } else { 122 | return { 123 | flags: ActionFlags.RefreshItems, 124 | searchPath, 125 | }; 126 | } 127 | }, 128 | }, 129 | clipMove: { 130 | description: "Move files from clipboard register.", 131 | callback: async ( 132 | args: { 133 | denops: Denops; 134 | items: DduItem[]; 135 | sourceOptions: SourceOptions; 136 | clipboard: Clipboard; 137 | actionHistory: ActionHistory; 138 | }, 139 | ) => { 140 | const cwd = await getTargetDirectory( 141 | args.denops, 142 | treePath2Filename(args.sourceOptions.path), 143 | args.items, 144 | ); 145 | 146 | const clipboard = await fn.getreg(args.denops, "+") as string; 147 | let searchPath = ""; 148 | for (const path of clipboard.split("\n")) { 149 | const dest = await checkOverwrite( 150 | args.denops, 151 | path, 152 | join(cwd, basename(path)), 153 | ); 154 | if (dest === "") { 155 | continue; 156 | } 157 | 158 | await safeAction("move", path, dest); 159 | 160 | searchPath = dest; 161 | } 162 | 163 | if (searchPath === "") { 164 | return ActionFlags.Persist; 165 | } else { 166 | return { 167 | flags: ActionFlags.RefreshItems, 168 | searchPath, 169 | }; 170 | } 171 | }, 172 | }, 173 | clipYank: { 174 | description: "Yank the file path to clipboard register.", 175 | callback: async (args: { denops: Denops; items: DduItem[] }) => { 176 | const paths = args.items.map((item) => { 177 | const action = item?.action as ActionData; 178 | return action.path ?? item.word; 179 | }); 180 | 181 | await fn.setreg(args.denops, "+", paths.join("\n"), "v"); 182 | 183 | return ActionFlags.Persist; 184 | }, 185 | }, 186 | copy: { 187 | description: "Copy the selected files to ddu clipboard.", 188 | callback: async ( 189 | args: { denops: Denops; items: DduItem[]; clipboard: Clipboard }, 190 | ) => { 191 | const message = `Copy to the clipboard: ${ 192 | args.items.length > 1 193 | ? args.items.length + " files" 194 | : getPath(args.items[0]) 195 | }`; 196 | 197 | await args.denops.call("ddu#kind#file#print", message); 198 | 199 | args.clipboard.action = "copy"; 200 | args.clipboard.items = args.items; 201 | args.clipboard.mode = ""; 202 | 203 | return ActionFlags.Persist; 204 | }, 205 | }, 206 | delete: { 207 | description: "Delete the file or directory.", 208 | callback: async ( 209 | args: { 210 | denops: Denops; 211 | items: DduItem[]; 212 | sourceOptions: SourceOptions; 213 | actionHistory: ActionHistory; 214 | }, 215 | ) => { 216 | const message = `Are you sure you want to delete ${ 217 | args.items.length > 1 218 | ? args.items.length + " files" 219 | : getPath(args.items[0]) 220 | }?`; 221 | 222 | const confirm = await args.denops.call( 223 | "ddu#kind#file#confirm", 224 | message, 225 | "&Yes\n&No\n&Cancel", 226 | 2, 227 | ) as number; 228 | if (confirm !== 1) { 229 | return ActionFlags.Persist; 230 | } 231 | 232 | args.actionHistory.actions = []; 233 | let searchPath = ""; 234 | for (const item of args.items) { 235 | const itemPath = getPath(item); 236 | 237 | searchPath = dirname(itemPath); 238 | 239 | await Deno.remove(itemPath, { recursive: true }); 240 | 241 | await args.denops.call( 242 | "ddu#kind#file#buffer_delete", 243 | await fn.bufnr(args.denops, itemPath), 244 | ); 245 | 246 | args.actionHistory.actions.push({ 247 | name: "delete", 248 | item, 249 | }); 250 | } 251 | 252 | return { 253 | flags: ActionFlags.RefreshItems, 254 | searchPath, 255 | }; 256 | }, 257 | }, 258 | execute: { 259 | description: "Execute the file.", 260 | callback: async ( 261 | args: { 262 | denops: Denops; 263 | actionParams: BaseParams; 264 | items: DduItem[]; 265 | sourceOptions: SourceOptions; 266 | }, 267 | ) => { 268 | const params = args.actionParams as ExecuteParams; 269 | const command = params.command ?? "edit"; 270 | 271 | for (const item of args.items) { 272 | const action = item?.action as ActionData; 273 | const path = action.path ?? item.word; 274 | 275 | await args.denops.call( 276 | "ddu#util#execute_path", 277 | command, 278 | path, 279 | ); 280 | } 281 | 282 | return ActionFlags.None; 283 | }, 284 | }, 285 | executeSystem: { 286 | description: "Execute the file by system associated command.", 287 | callback: async ( 288 | args: { 289 | denops: Denops; 290 | actionParams: BaseParams; 291 | items: DduItem[]; 292 | sourceOptions: SourceOptions; 293 | }, 294 | ) => { 295 | const params = args.actionParams as ExecuteSystemParams; 296 | const method = params.method ?? ""; 297 | 298 | for (const item of args.items) { 299 | const action = item?.action as ActionData; 300 | const path = action.path ?? item.word; 301 | await args.denops.call("ddu#kind#file#open", path, method); 302 | } 303 | 304 | return ActionFlags.Persist; 305 | }, 306 | }, 307 | feedkeys: { 308 | description: 309 | "Use |feedkeys()| to insert the path.\nIt is useful in command line mode.", 310 | callback: async (args: { denops: Denops; items: DduItem[] }) => { 311 | for (const item of args.items) { 312 | await feedkeys(args.denops, item); 313 | } 314 | return ActionFlags.None; 315 | }, 316 | }, 317 | insert: { 318 | description: "Paste the path like |P|.", 319 | callback: async ( 320 | args: { denops: Denops; context: Context; items: DduItem[] }, 321 | ) => { 322 | for (const item of args.items) { 323 | await paste(args.denops, item, "P"); 324 | } 325 | return ActionFlags.None; 326 | }, 327 | }, 328 | link: { 329 | description: "Create link the selected files to ddu clipboard.", 330 | callback: async (args: { 331 | denops: Denops; 332 | actionParams: BaseParams; 333 | items: DduItem[]; 334 | clipboard: Clipboard; 335 | }) => { 336 | const params = args.actionParams as LinkParams; 337 | const mode = params.mode ?? "absolute"; 338 | const message = `Link to the clipboard: ${ 339 | args.items.length > 1 340 | ? args.items.length + " files" 341 | : getPath(args.items[0]) 342 | }`; 343 | 344 | await args.denops.call("ddu#kind#file#print", message); 345 | 346 | args.clipboard.action = "link"; 347 | args.clipboard.items = args.items; 348 | args.clipboard.mode = mode; 349 | 350 | return ActionFlags.Persist; 351 | }, 352 | }, 353 | loclist: { 354 | description: "Set the |location-list| and open the |location-list| window.", 355 | callback: async (args: { denops: Denops; items: DduItem[] }) => { 356 | const qfloclist: QuickFix[] = buildQfLocList(args.items); 357 | 358 | if (qfloclist.length !== 0) { 359 | await fn.setloclist(args.denops, 0, qfloclist, " "); 360 | await args.denops.cmd("lopen"); 361 | } 362 | 363 | return ActionFlags.None; 364 | }, 365 | }, 366 | move: { 367 | description: "Move the selected files to ddu clipboard.", 368 | callback: async ( 369 | args: { denops: Denops; items: DduItem[]; clipboard: Clipboard }, 370 | ) => { 371 | const message = `Move to the clipboard: ${ 372 | args.items.length > 1 373 | ? args.items.length + " files" 374 | : getPath(args.items[0]) 375 | }`; 376 | 377 | await args.denops.call("ddu#kind#file#print", message); 378 | 379 | args.clipboard.action = "move"; 380 | args.clipboard.items = args.items; 381 | args.clipboard.mode = ""; 382 | 383 | return ActionFlags.Persist; 384 | }, 385 | }, 386 | narrow: { 387 | description: "Change |ddu-source-option-path| to the directory.", 388 | callback: async (args: { 389 | denops: Denops; 390 | options: DduOptions; 391 | actionParams: BaseParams; 392 | sourceOptions: SourceOptions; 393 | items: DduItem[]; 394 | }) => { 395 | const params = args.actionParams as NarrowParams; 396 | if (params.path) { 397 | if (params.path === "..") { 398 | let current = treePath2Filename(args.sourceOptions.path); 399 | if (current === "") { 400 | current = await fn.getcwd(args.denops) as string; 401 | } 402 | args.sourceOptions.path = normalize(join(current, "..")); 403 | return { 404 | flags: ActionFlags.RefreshItems, 405 | searchPath: current, 406 | }; 407 | } else { 408 | args.sourceOptions.path = params.path; 409 | return ActionFlags.RefreshItems; 410 | } 411 | } 412 | 413 | if (args.items.length > 1) { 414 | await args.denops.call("ddu#start", { 415 | name: args.options.name, 416 | push: true, 417 | sources: await Promise.all(args.items.map(async (item) => { 418 | return { 419 | name: "file", 420 | options: { 421 | columns: args.sourceOptions.columns, 422 | path: await getDirectory(item), 423 | }, 424 | }; 425 | })), 426 | }); 427 | 428 | return ActionFlags.None; 429 | } 430 | 431 | const dir = await getDirectory(args.items[0]); 432 | if (dir !== "") { 433 | args.sourceOptions.path = dir; 434 | return ActionFlags.RefreshItems; 435 | } 436 | 437 | return ActionFlags.None; 438 | }, 439 | }, 440 | newDirectory: { 441 | description: 442 | "Make new directory in expanded directory tree or current directory.", 443 | callback: async ( 444 | args: { 445 | denops: Denops; 446 | items: DduItem[]; 447 | sourceOptions: SourceOptions; 448 | actionHistory: ActionHistory; 449 | }, 450 | ) => { 451 | const cwd = await getTargetDirectory( 452 | args.denops, 453 | treePath2Filename(args.sourceOptions.path), 454 | args.items, 455 | ); 456 | 457 | const input = await args.denops.call( 458 | "ddu#kind#file#cwd_input", 459 | cwd, 460 | "Please input directory names(comma separated): ", 461 | "", 462 | "dir", 463 | ) as string; 464 | if (input === "") { 465 | return ActionFlags.Persist; 466 | } 467 | 468 | let newDirectory = ""; 469 | 470 | args.actionHistory.actions = []; 471 | for (const name of input.split(",")) { 472 | newDirectory = isAbsolute(name) ? name : join(cwd, name); 473 | 474 | // Exists check 475 | if (await exists(newDirectory)) { 476 | await args.denops.call( 477 | "ddu#kind#file#print", 478 | `${newDirectory} already exists.`, 479 | ); 480 | 481 | continue; 482 | } 483 | 484 | await ensureDir(newDirectory); 485 | 486 | args.actionHistory.actions.push({ 487 | name: "newDirectory", 488 | dest: newDirectory, 489 | }); 490 | } 491 | 492 | if (newDirectory === "") { 493 | return ActionFlags.Persist; 494 | } 495 | 496 | return { 497 | flags: ActionFlags.RefreshItems, 498 | searchPath: newDirectory, 499 | }; 500 | }, 501 | }, 502 | newFile: { 503 | description: 504 | "Make new file in expanded directory tree or current directory.", 505 | callback: async ( 506 | args: { 507 | denops: Denops; 508 | items: DduItem[]; 509 | sourceOptions: SourceOptions; 510 | actionHistory: ActionHistory; 511 | }, 512 | ) => { 513 | const cwd = await getTargetDirectory( 514 | args.denops, 515 | treePath2Filename(args.sourceOptions.path), 516 | args.items, 517 | ); 518 | 519 | const input = await args.denops.call( 520 | "ddu#kind#file#cwd_input", 521 | cwd, 522 | "Please input names(comma separated): ", 523 | "", 524 | "file", 525 | ) as string; 526 | if (input === "") { 527 | return ActionFlags.Persist; 528 | } 529 | 530 | let newFile = ""; 531 | 532 | args.actionHistory.actions = []; 533 | for (const name of input.split(",")) { 534 | newFile = isAbsolute(name) ? name : join(cwd, name); 535 | 536 | // Exists check 537 | if (await exists(newFile)) { 538 | await args.denops.call( 539 | "ddu#kind#file#print", 540 | `${newFile} already exists.`, 541 | ); 542 | continue; 543 | } 544 | 545 | if (newFile.slice(-1) === "/") { 546 | await ensureDir(newFile); 547 | } else { 548 | await ensureFile(newFile); 549 | } 550 | 551 | args.actionHistory.actions.push({ 552 | name: "newFile", 553 | dest: newFile, 554 | }); 555 | } 556 | 557 | if (newFile === "") { 558 | return ActionFlags.Persist; 559 | } 560 | 561 | return { 562 | flags: ActionFlags.RefreshItems, 563 | searchPath: newFile, 564 | }; 565 | }, 566 | }, 567 | open: { 568 | description: 569 | "Open the items.\nIf the item is buffer, switch to the buffer.\n" + 570 | "If the item is file, open the file.", 571 | callback: async (args: { 572 | denops: Denops; 573 | context: Context; 574 | actionParams: BaseParams; 575 | items: DduItem[]; 576 | }) => { 577 | const params = args.actionParams as OpenParams; 578 | const openCommand = params.command ?? "edit"; 579 | 580 | // Save current position. 581 | await args.denops.cmd("normal! m`"); 582 | 583 | for (const item of args.items) { 584 | const action = item?.action as ActionData; 585 | const bufNr = action.bufNr ?? 586 | await args.denops.call( 587 | "ddu#kind#file#bufnr", 588 | action.path ?? item.word, 589 | ) as number; 590 | 591 | if (bufNr >= 0) { 592 | if (openCommand !== "edit") { 593 | await args.denops.call( 594 | "ddu#util#execute_path", 595 | openCommand, 596 | action.path ?? "", 597 | ); 598 | } 599 | 600 | // NOTE: "bufNr" may be hidden 601 | const loaded = await fn.bufloaded(args.denops, bufNr); 602 | if (!loaded) { 603 | await fn.bufload(args.denops, bufNr); 604 | } 605 | await args.denops.cmd(`buffer ${bufNr}`); 606 | if (!loaded) { 607 | await fn.setbufvar(args.denops, bufNr, "&buflisted", 1); 608 | } 609 | } else if (action.path) { 610 | // Check the file is binary file or too big. 611 | const stat = await safeStat(action.path); 612 | if (stat && stat.isDirectory) { 613 | await args.denops.call( 614 | "ddu#kind#file#print", 615 | `${action.path} is directory.`, 616 | ); 617 | continue; 618 | } 619 | 620 | if (stat && await isBinary(action.path, stat)) { 621 | const confirm = await args.denops.call( 622 | "ddu#kind#file#confirm", 623 | `"${action.path}" has binary code. Opening?`, 624 | "&Yes\n&No\n&Cancel", 625 | 2, 626 | ) as number; 627 | if (confirm !== 1) { 628 | continue; 629 | } 630 | } 631 | 632 | const maxSize = params.maxSize ?? 500000; 633 | if (stat && stat.size > maxSize) { 634 | const confirm = await args.denops.call( 635 | "ddu#kind#file#confirm", 636 | `"${action.path}" ${stat.size} bytes are too huge. Opening?`, 637 | "&Yes\n&No\n&Cancel", 638 | 2, 639 | ) as number; 640 | if (confirm !== 1) { 641 | continue; 642 | } 643 | } 644 | 645 | await args.denops.call( 646 | "ddu#util#execute_path", 647 | openCommand, 648 | action.path, 649 | ); 650 | } 651 | 652 | const mode = await fn.mode(args.denops); 653 | if (action.lineNr !== undefined) { 654 | await fn.cursor(args.denops, action.lineNr, 0); 655 | 656 | if (args.context.input !== "") { 657 | // Search the input text 658 | const text = (await fn.getline(args.denops, ".")).toLowerCase(); 659 | const input = args.context.input.toLowerCase(); 660 | await fn.cursor( 661 | args.denops, 662 | 0, 663 | text.indexOf(input) + 1 + (mode === "i" ? 1 : 0), 664 | ); 665 | } 666 | } 667 | 668 | if (action.col !== undefined) { 669 | // If it is insert mode, it needs adjust. 670 | await fn.cursor( 671 | args.denops, 672 | 0, 673 | action.col + (mode === "i" ? 1 : 0), 674 | ); 675 | } 676 | 677 | // NOTE: Open folds and centering 678 | await args.denops.cmd("normal! zvzz"); 679 | } 680 | 681 | return ActionFlags.None; 682 | }, 683 | }, 684 | paste: { 685 | description: "Fire the clipboard action in the current directory.", 686 | callback: async ( 687 | args: { 688 | denops: Denops; 689 | items: DduItem[]; 690 | sourceOptions: SourceOptions; 691 | clipboard: Clipboard; 692 | actionHistory: ActionHistory; 693 | }, 694 | ) => { 695 | const cwd = await getTargetDirectory( 696 | args.denops, 697 | treePath2Filename(args.sourceOptions.path), 698 | args.items, 699 | ); 700 | 701 | let searchPath = ""; 702 | args.actionHistory.actions = []; 703 | switch (args.clipboard.action) { 704 | case "copy": 705 | case "move": 706 | case "link": 707 | for (const item of args.clipboard.items) { 708 | const action = item?.action as ActionData; 709 | const path = action.path ?? item.word; 710 | 711 | const dest = await checkOverwrite( 712 | args.denops, 713 | path, 714 | join(cwd, basename(path)), 715 | ); 716 | if (dest === "") { 717 | continue; 718 | } 719 | 720 | if (args.clipboard.action === "link") { 721 | // Exists check 722 | if (await exists(dest)) { 723 | await args.denops.call( 724 | "ddu#kind#file#print", 725 | `${dest} already exists.`, 726 | ); 727 | return ActionFlags.Persist; 728 | } 729 | 730 | switch (args.clipboard.mode) { 731 | case "hard": 732 | await Deno.link(path, dest); 733 | break; 734 | case "absolute": 735 | await Deno.symlink(path, dest, { 736 | type: await isDirectory(path) ? "dir" : "file", 737 | }); 738 | break; 739 | case "relative": 740 | await Deno.symlink(relative(path, dirname(dest)), dest, { 741 | type: await isDirectory(path) ? "dir" : "file", 742 | }); 743 | break; 744 | default: 745 | await args.denops.call( 746 | "ddu#kind#file#print", 747 | `Invalid mode: ${args.clipboard.mode}`, 748 | ); 749 | return ActionFlags.Persist; 750 | } 751 | } else { 752 | await safeAction(args.clipboard.action, path, dest); 753 | 754 | if (args.clipboard.action === "move") { 755 | await args.denops.call( 756 | "ddu#kind#file#buffer_rename", 757 | await fn.bufnr(args.denops, path), 758 | dest, 759 | ); 760 | } 761 | } 762 | 763 | searchPath = dest; 764 | 765 | args.actionHistory.actions.push({ 766 | name: args.clipboard.action, 767 | item, 768 | dest, 769 | }); 770 | } 771 | break; 772 | default: 773 | await args.denops.call( 774 | "ddu#kind#file#print", 775 | `Invalid action: ${args.clipboard.action}`, 776 | ); 777 | return ActionFlags.Persist; 778 | } 779 | 780 | if (searchPath === "") { 781 | return ActionFlags.Persist; 782 | } else { 783 | return { 784 | flags: ActionFlags.RefreshItems, 785 | searchPath, 786 | }; 787 | } 788 | }, 789 | }, 790 | quickfix: { 791 | description: "Set the |quickfix| list and open the |quickfix| window.", 792 | callback: async (args: { denops: Denops; items: DduItem[] }) => { 793 | const qfloclist: QuickFix[] = buildQfLocList(args.items); 794 | 795 | if (qfloclist.length !== 0) { 796 | await fn.setqflist(args.denops, qfloclist, " "); 797 | await args.denops.cmd("copen"); 798 | } 799 | 800 | return ActionFlags.None; 801 | }, 802 | }, 803 | rename: { 804 | description: 805 | "Rename the file/directory under cursor or from selected list.", 806 | callback: async (args: { 807 | denops: Denops; 808 | options: DduOptions; 809 | items: DduItem[]; 810 | sourceOptions: SourceOptions; 811 | actionHistory: ActionHistory; 812 | }) => { 813 | if (args.items.length > 1) { 814 | // Use exrename instead 815 | await args.denops.call( 816 | "ddu#kind#file#exrename#create_buffer", 817 | args.items.map((item) => { 818 | return { 819 | action__path: (item?.action as ActionData).path ?? item.word, 820 | }; 821 | }), 822 | { 823 | name: args.options.name, 824 | }, 825 | ); 826 | 827 | return ActionFlags.Persist; 828 | } 829 | 830 | let cwd = args.sourceOptions.path; 831 | if (cwd === "") { 832 | cwd = await fn.getcwd(args.denops) as string; 833 | } 834 | 835 | let newPath = ""; 836 | args.actionHistory.actions = []; 837 | for (const item of args.items) { 838 | const action = item?.action as ActionData; 839 | const path = action.path ?? item.word; 840 | 841 | newPath = await args.denops.call( 842 | "ddu#kind#file#cwd_input", 843 | cwd, 844 | `Please input a new name: ${path} -> `, 845 | path, 846 | (await isDirectory(path)) ? "dir" : "file", 847 | ) as string; 848 | 849 | if (newPath === "" || path === newPath) { 850 | continue; 851 | } 852 | 853 | await safeAction("rename", path, newPath); 854 | 855 | await args.denops.call( 856 | "ddu#kind#file#buffer_rename", 857 | await fn.bufnr(args.denops, path), 858 | newPath, 859 | ); 860 | 861 | args.actionHistory.actions.push({ 862 | name: "rename", 863 | item, 864 | dest: newPath, 865 | }); 866 | } 867 | 868 | return { 869 | flags: ActionFlags.RefreshItems, 870 | searchPath: newPath, 871 | }; 872 | }, 873 | }, 874 | trash: { 875 | description: "Move the file or directory to the trash.", 876 | callback: async ( 877 | args: { 878 | denops: Denops; 879 | items: DduItem[]; 880 | sourceOptions: SourceOptions; 881 | kindParams: Params; 882 | actionHistory: ActionHistory; 883 | }, 884 | ) => { 885 | const message = `Are you sure you want to move to the trash ${ 886 | args.items.length > 1 887 | ? args.items.length + " files" 888 | : getPath(args.items[0]) 889 | }?`; 890 | 891 | const confirm = await args.denops.call( 892 | "ddu#kind#file#confirm", 893 | message, 894 | "&Yes\n&No\n&Cancel", 895 | 2, 896 | ) as number; 897 | if (confirm !== 1) { 898 | return ActionFlags.Persist; 899 | } 900 | 901 | const trashCommand = args.kindParams.trashCommand; 902 | 903 | if (!await fn.executable(args.denops, trashCommand[0])) { 904 | await printError( 905 | args.denops, 906 | `${trashCommand[0]} is not found.`, 907 | ); 908 | return ActionFlags.Persist; 909 | } 910 | 911 | args.actionHistory.actions = []; 912 | for (const item of args.items) { 913 | const cmd = Array.from(trashCommand); 914 | cmd.push(getPath(item)); 915 | const proc = new Deno.Command( 916 | cmd[0], 917 | { 918 | args: cmd.slice(1), 919 | stdout: "null", 920 | stderr: "piped", 921 | stdin: "null", 922 | }, 923 | ).spawn(); 924 | 925 | proc.status.then(async (s) => { 926 | if (s.success) { 927 | await args.denops.call( 928 | "ddu#kind#file#buffer_delete", 929 | await fn.bufnr(args.denops, getPath(item)), 930 | ); 931 | return; 932 | } 933 | 934 | await printError( 935 | args.denops, 936 | `Run ${cmd} is failed with exit code ${s.code}.`, 937 | ); 938 | const err = []; 939 | for await (const line of iterLine(proc.stderr)) { 940 | err.push(line); 941 | } 942 | await printError( 943 | args.denops, 944 | err.join("\n"), 945 | ); 946 | }); 947 | 948 | args.actionHistory.actions.push({ 949 | name: "trash", 950 | item, 951 | }); 952 | } 953 | 954 | return ActionFlags.RefreshItems; 955 | }, 956 | }, 957 | undo: { 958 | description: "Undo the previous action.", 959 | callback: async ( 960 | args: { 961 | denops: Denops; 962 | items: DduItem[]; 963 | sourceOptions: SourceOptions; 964 | actionHistory: ActionHistory; 965 | }, 966 | ) => { 967 | if (args.actionHistory.actions.length === 0) { 968 | return ActionFlags.Persist; 969 | } 970 | 971 | let searchPath = ""; 972 | 973 | const actions: typeof args.actionHistory.actions = []; 974 | 975 | const message = `Are you sure you want to undo ${ 976 | args.actionHistory.actions.map( 977 | (action) => action.name + ":" + action.dest, 978 | ) 979 | } ${args.actionHistory.actions.length > 1 ? "actions" : "action"}?`; 980 | 981 | const confirm = await args.denops.call( 982 | "ddu#kind#file#confirm", 983 | message, 984 | "&Yes\n&No\n&Cancel", 985 | 2, 986 | ) as number; 987 | if (confirm !== 1) { 988 | return ActionFlags.Persist; 989 | } 990 | 991 | for (const action of args.actionHistory.actions.reverse()) { 992 | switch (action.name) { 993 | case "copy": 994 | case "link": 995 | case "newDirectory": 996 | case "newFile": 997 | if (action.dest) { 998 | await Deno.remove(action.dest, { recursive: true }); 999 | 1000 | actions.push({ 1001 | name: "delete", 1002 | item: action.item, 1003 | }); 1004 | } 1005 | break; 1006 | case "move": 1007 | case "rename": 1008 | if (action.dest && action.item) { 1009 | await move( 1010 | action.dest, 1011 | getPath(action.item), 1012 | ); 1013 | searchPath = getPath(action.item); 1014 | 1015 | actions.push({ 1016 | name: action.name, 1017 | dest: getPath(action.item), 1018 | item: { 1019 | ...action.item, 1020 | word: action.dest, 1021 | action: { 1022 | path: action.dest, 1023 | }, 1024 | treePath: action.dest, 1025 | }, 1026 | }); 1027 | } 1028 | break; 1029 | default: 1030 | await args.denops.call( 1031 | "ddu#kind#file#print", 1032 | `Cannot undo action: ${action.name}`, 1033 | ); 1034 | return ActionFlags.Persist; 1035 | } 1036 | } 1037 | 1038 | // Clear 1039 | args.actionHistory.actions = actions; 1040 | 1041 | return { 1042 | flags: ActionFlags.RefreshItems, 1043 | searchPath, 1044 | }; 1045 | }, 1046 | }, 1047 | yank: { 1048 | description: "Yank the file path to unnamed register.", 1049 | callback: async (args: { denops: Denops; items: DduItem[] }) => { 1050 | for (const item of args.items) { 1051 | const action = item?.action as ActionData; 1052 | const path = action.path ?? item.word; 1053 | 1054 | await fn.setreg(args.denops, '"', path, "v"); 1055 | await fn.setreg( 1056 | args.denops, 1057 | await vars.v.get(args.denops, "register"), 1058 | path, 1059 | "v", 1060 | ); 1061 | } 1062 | 1063 | return ActionFlags.Persist; 1064 | }, 1065 | }, 1066 | }; 1067 | 1068 | type Params = { 1069 | trashCommand: string[]; 1070 | }; 1071 | 1072 | type NarrowParams = { 1073 | path: string; 1074 | }; 1075 | 1076 | type ExecuteParams = { 1077 | command: string; 1078 | }; 1079 | 1080 | type ExecuteSystemParams = { 1081 | method: string; 1082 | }; 1083 | 1084 | type OpenParams = { 1085 | command: string; 1086 | maxSize?: number; 1087 | }; 1088 | 1089 | type LinkParams = { 1090 | mode: "hard" | "absolute" | "relative"; 1091 | }; 1092 | 1093 | type QuickFix = { 1094 | lnum: number; 1095 | text: string; 1096 | col?: number; 1097 | bufnr?: number; 1098 | filename?: string; 1099 | }; 1100 | 1101 | type PreviewOption = { 1102 | previewCmds?: string[]; 1103 | maxSize?: number; 1104 | }; 1105 | 1106 | export class Kind extends BaseKind { 1107 | override actions = FileActions; 1108 | 1109 | override async getPreviewer(args: { 1110 | denops: Denops; 1111 | item: DduItem; 1112 | actionParams: BaseParams; 1113 | previewContext: PreviewContext; 1114 | }): Promise { 1115 | const action = args.item.action as ActionData; 1116 | if (!action) { 1117 | return undefined; 1118 | } 1119 | 1120 | const param = unknownEnsure(args.actionParams, is.Record) as PreviewOption; 1121 | 1122 | if (action.path && param.previewCmds?.length) { 1123 | const previewHeight = args.previewContext.height; 1124 | let startLine = 0; 1125 | let lineNr = 0; 1126 | if (action.lineNr) { 1127 | lineNr = action.lineNr; 1128 | startLine = Math.max( 1129 | 0, 1130 | Math.ceil(action.lineNr - previewHeight / 2), 1131 | ); 1132 | } 1133 | 1134 | const pairs: Record = { 1135 | s: action.path, 1136 | l: String(lineNr), 1137 | h: String(previewHeight), 1138 | e: String(startLine + previewHeight), 1139 | b: String(startLine), 1140 | "%": "%", 1141 | }; 1142 | const replacer = ( 1143 | match: string, 1144 | p1: string, 1145 | ) => { 1146 | if (!p1.length || !(p1 in pairs)) { 1147 | throw `invalid item ${match}`; 1148 | } 1149 | return pairs[p1]; 1150 | }; 1151 | const replaced: string[] = []; 1152 | try { 1153 | for (const cmd of param.previewCmds) { 1154 | replaced.push(cmd.replace(/%(.?)/g, replacer)); 1155 | } 1156 | } catch (e: unknown) { 1157 | return { 1158 | kind: "nofile", 1159 | contents: e?.toString ? ["Error", e.toString()] : [], 1160 | highlights: [{ 1161 | name: "ddu-kind-file-error", 1162 | hl_group: "Error", 1163 | row: 1, 1164 | col: 1, 1165 | width: 5, 1166 | }], 1167 | }; 1168 | } 1169 | 1170 | return { 1171 | kind: "terminal", 1172 | cmds: replaced, 1173 | }; 1174 | } 1175 | 1176 | // File path check 1177 | if (action.path) { 1178 | const stat = await safeStat(action.path); 1179 | if (stat && stat.isDirectory) { 1180 | // Directory. 1181 | return { 1182 | kind: "nofile", 1183 | contents: [`${action.path} is directory`], 1184 | }; 1185 | } 1186 | 1187 | if (stat && await isBinary(action.path, stat)) { 1188 | // Binary file. 1189 | return { 1190 | kind: "nofile", 1191 | contents: [`${action.path} is binary file`], 1192 | }; 1193 | } 1194 | 1195 | const maxSize = param.maxSize ?? 500000; 1196 | if (stat && stat.size > maxSize) { 1197 | // Over maxSize file. 1198 | return { 1199 | kind: "nofile", 1200 | contents: [`${action.path} is over maxSize.`], 1201 | }; 1202 | } 1203 | } 1204 | 1205 | if (action.bufNr) { 1206 | // NOTE: buffer may be hidden 1207 | await fn.bufload(args.denops, action.bufNr); 1208 | } 1209 | 1210 | return { 1211 | kind: "buffer", 1212 | path: action.bufNr ? undefined : action.path, 1213 | expr: action.bufNr, 1214 | lineNr: action.lineNr, 1215 | }; 1216 | } 1217 | 1218 | override params(): Params { 1219 | return { 1220 | trashCommand: ["gio", "trash"], 1221 | }; 1222 | } 1223 | } 1224 | 1225 | const buildQfLocList = (items: DduItem[]) => { 1226 | const qfloclist: QuickFix[] = []; 1227 | 1228 | for (const item of items) { 1229 | const action = item?.action as ActionData; 1230 | 1231 | const qfloc = { 1232 | text: item.word, 1233 | } as QuickFix; 1234 | 1235 | if (action.lineNr) { 1236 | qfloc.lnum = action.lineNr; 1237 | } 1238 | if (action.col) { 1239 | qfloc.col = action.col; 1240 | } 1241 | if (action.bufNr) { 1242 | qfloc.bufnr = action.bufNr; 1243 | } 1244 | if (action.path) { 1245 | qfloc.filename = action.path; 1246 | } 1247 | if (action.text) { 1248 | qfloc.text = action.text; 1249 | } 1250 | 1251 | qfloclist.push(qfloc); 1252 | } 1253 | 1254 | return qfloclist; 1255 | }; 1256 | 1257 | const getTargetDirectory = async ( 1258 | denops: Denops, 1259 | initPath: string, 1260 | items: DduItem[], 1261 | ) => { 1262 | let dir = initPath; 1263 | for (const item of items) { 1264 | const action = item?.action as ActionData; 1265 | const path = action.path ?? item.word; 1266 | 1267 | dir = item.__expanded ? path : dirname(path); 1268 | } 1269 | 1270 | if (dir === "") { 1271 | dir = await fn.getcwd(denops) as string; 1272 | } 1273 | 1274 | return dir; 1275 | }; 1276 | 1277 | const getDirectory = async (item: DduItem) => { 1278 | const action = item?.action as ActionData; 1279 | 1280 | // NOTE: Deno.stat() may be failed 1281 | try { 1282 | const path = action.path ?? item.word; 1283 | const dir = (action.isDirectory ?? (await Deno.stat(path)).isDirectory) 1284 | ? path 1285 | : dirname(path); 1286 | if ((await Deno.stat(dir)).isDirectory) { 1287 | return dir; 1288 | } 1289 | } catch (_e: unknown) { 1290 | // Ignore 1291 | } 1292 | 1293 | return ""; 1294 | }; 1295 | 1296 | const getPath = (item: DduItem) => { 1297 | const action = item?.action as ActionData; 1298 | return action.path ?? item.word; 1299 | }; 1300 | 1301 | const safeStat = async (path: string): Promise => { 1302 | // NOTE: Deno.stat() may be failed 1303 | try { 1304 | const stat = await Deno.stat(path); 1305 | return stat; 1306 | } catch (_: unknown) { 1307 | // Ignore stat exception 1308 | } 1309 | return null; 1310 | }; 1311 | 1312 | const exists = async (path: string) => { 1313 | // NOTE: Deno.stat() may be failed 1314 | try { 1315 | const stat = await Deno.stat(path); 1316 | if (stat.isDirectory || stat.isFile || stat.isSymlink) { 1317 | return true; 1318 | } 1319 | } catch (_: unknown) { 1320 | // Ignore stat exception 1321 | } 1322 | 1323 | return false; 1324 | }; 1325 | 1326 | const isDirectory = async (path: string) => { 1327 | // NOTE: Deno.stat() may be failed 1328 | try { 1329 | if ((await Deno.stat(path)).isDirectory) { 1330 | return true; 1331 | } 1332 | } catch (_e: unknown) { 1333 | // Ignore 1334 | } 1335 | 1336 | return false; 1337 | }; 1338 | 1339 | const isBinary = async ( 1340 | path: string, 1341 | stat: Deno.FileInfo, 1342 | ): Promise => { 1343 | if (!stat.isFile || stat.size === 0) { 1344 | return false; 1345 | } 1346 | 1347 | const file = await Deno.open(path, { read: true }); 1348 | const rangedStream = file.readable 1349 | .pipeThrough( 1350 | new ByteSliceStream( 1351 | 0, 1352 | Math.min(stat.size, 256) - 1, 1353 | ), 1354 | ); 1355 | const range = await toArrayBuffer(rangedStream); 1356 | const decoder = new TextDecoder("utf-8"); 1357 | const text = decoder.decode(range); 1358 | 1359 | // deno-lint-ignore no-control-regex 1360 | return text.match(/[\x00-\x08\x10-\x1a\x1c-\x1f]{2,}/) !== null; 1361 | }; 1362 | 1363 | const checkOverwrite = async ( 1364 | denops: Denops, 1365 | src: string, 1366 | dest: string, 1367 | ): Promise => { 1368 | const sStat = await safeStat(src); 1369 | const dStat = await safeStat(dest); 1370 | 1371 | if (!sStat) { 1372 | return ""; 1373 | } 1374 | if (!dStat) { 1375 | return dest; 1376 | } 1377 | 1378 | const message = ` src: ${src} ${sStat.size} bytes\n` + 1379 | ` ${sStat.mtime?.toISOString()}\n` + 1380 | `dest: ${dest} ${dStat.size} bytes\n` + 1381 | ` ${dStat.mtime?.toISOString()}\n` + 1382 | `${dest} already exists. Overwrite?`; 1383 | 1384 | const confirm = await denops.call( 1385 | "ddu#kind#file#confirm", 1386 | message, 1387 | "&Force\n&Time\n&Underbar\n&No\n&Rename", 1388 | 4, 1389 | ) as number; 1390 | 1391 | let ret = ""; 1392 | 1393 | switch (confirm) { 1394 | case 1: 1395 | ret = dest; 1396 | break; 1397 | case 2: 1398 | if (dStat.mtime && sStat.mtime && dStat.mtime < sStat.mtime) { 1399 | ret = src; 1400 | } 1401 | break; 1402 | case 3: 1403 | ret = dest + "_"; 1404 | break; 1405 | case 4: 1406 | break; 1407 | case 5: 1408 | ret = await denops.call( 1409 | "ddu#kind#file#cwd_input", 1410 | "", 1411 | `Please input a new name: ${dest} -> `, 1412 | dest, 1413 | sStat.isDirectory ? "dir" : "file", 1414 | ) as string; 1415 | if (ret === dest) { 1416 | ret = ""; 1417 | } 1418 | break; 1419 | } 1420 | 1421 | return ret; 1422 | }; 1423 | 1424 | const paste = async (denops: Denops, item: DduItem, pasteKey: string) => { 1425 | const action = item?.action as ActionData; 1426 | 1427 | if (action.path === null) { 1428 | return; 1429 | } 1430 | 1431 | const oldReg = await fn.getreginfo(denops, '"'); 1432 | 1433 | await fn.setreg(denops, '"', action.path, "v"); 1434 | try { 1435 | await denops.cmd('normal! ""' + pasteKey); 1436 | } finally { 1437 | await fn.setreg(denops, '"', oldReg); 1438 | } 1439 | 1440 | // Open folds 1441 | await denops.cmd("normal! zv"); 1442 | }; 1443 | 1444 | const feedkeys = async (denops: Denops, item: DduItem) => { 1445 | const action = item?.action as ActionData; 1446 | 1447 | if (action.path === null) { 1448 | return; 1449 | } 1450 | 1451 | // Use feedkeys() instead 1452 | await fn.feedkeys(denops, action.path, "n"); 1453 | }; 1454 | 1455 | const safeAction = async ( 1456 | action: "rename" | "move" | "copy", 1457 | src: string, 1458 | dest: string, 1459 | ) => { 1460 | // Exists check 1461 | if (action !== "copy" && await exists(dest)) { 1462 | // NOTE: "src" may be same with "dest". Rename is needed. 1463 | const temp = src + "___"; 1464 | 1465 | await Deno.rename(src, temp); 1466 | 1467 | // NOTE: if src === dest, it may be not exists 1468 | if (await exists(dest)) { 1469 | await Deno.remove(dest, { recursive: true }); 1470 | } 1471 | 1472 | src = temp; 1473 | } 1474 | 1475 | if (!await exists(dirname(dest))) { 1476 | return; 1477 | } 1478 | 1479 | switch (action) { 1480 | case "rename": 1481 | await Deno.rename(src, dest); 1482 | break; 1483 | case "move": 1484 | await move(src, dest); 1485 | break; 1486 | case "copy": 1487 | await copy(src, dest, { overwrite: true }); 1488 | break; 1489 | } 1490 | }; 1491 | 1492 | async function* iterLine(r: ReadableStream): AsyncIterable { 1493 | const lines = r 1494 | .pipeThrough(new TextDecoderStream(), { 1495 | preventCancel: false, 1496 | preventClose: false, 1497 | }) 1498 | .pipeThrough(new TextLineStream()); 1499 | 1500 | for await (const line of lines) { 1501 | if ((line as string).length) { 1502 | yield line as string; 1503 | } 1504 | } 1505 | } 1506 | --------------------------------------------------------------------------------