dirvish.vim 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  1. let s:srcdir = expand('<sfile>:h:h:p')
  2. let s:sep = exists('+shellslash') && !&shellslash ? '\' : '/'
  3. let s:noswapfile = (2 == exists(':noswapfile')) ? 'noswapfile' : ''
  4. let s:noau = 'silent noautocmd keepjumps'
  5. let s:cb_map = {} " callback map
  6. let s:rel = get(g:, 'dirvish_relative_paths', 0)
  7. " Debug:
  8. " echo '' > dirvish.log ; tail -F dirvish.log
  9. " nvim +"let g:dirvish_dbg=1" -- b1 b2
  10. " :bnext
  11. " -
  12. if get(g:, 'dirvish_dbg')
  13. func! s:log(msg, ...) abort
  14. call writefile([a:msg], expand('~/dirvish.log'), 'as')
  15. endf
  16. else
  17. func! s:log(msg, ...) abort
  18. endf
  19. endif
  20. func! s:msg_error(msg) abort
  21. redraw | echohl ErrorMsg | echomsg 'dirvish:' a:msg | echohl None
  22. endf
  23. func! s:eq(dir1, dir2) abort
  24. return fnamemodify(a:dir1, ':p') ==# fnamemodify(a:dir2, ':p')
  25. endf
  26. " Gets full path, or relative if g:dirvish_relative_paths=1.
  27. func! s:f(f) abort
  28. let f = fnamemodify(a:f, s:rel ? ':p:.' : ':p')
  29. " Special case: ":p:." yields empty for CWD.
  30. return !empty(f) ? f : fnamemodify(a:f, ':p')
  31. endf
  32. func! s:suf() abort
  33. let m = get(g:, 'dirvish_mode', 1)
  34. return type(m) == type(0) && m <= 1 ? 1 : 0
  35. endf
  36. " Normalizes slashes:
  37. " - Replace "\" with "/", for safe use of fnameescape(), isdirectory(). Vim bug #541.
  38. " - Collapse slashes (except UNC-style \\foo\bar).
  39. " - Always end dir with "/".
  40. " - Special case: empty string (CWD) => "./".
  41. func! s:sl(f) abort
  42. let f = has('win32') ? tr(a:f, '\', '/') : a:f
  43. " Collapse slashes (except UNC-style \\foo\bar).
  44. let f = f[0] . substitute(f[1:], '/\+', '/', 'g')
  45. " End with separator.
  46. return empty(f) ? './' : (f[-1:] !=# '/' && isdirectory(f) ? f.'/' : f)
  47. endf
  48. " Workaround for platform quirks, and shows an error if dir is invalid.
  49. func! s:fix_dir(dir, silent) abort
  50. let dir = s:sl(a:dir)
  51. if !isdirectory(dir)
  52. " Fallback for cygwin/MSYS paths lacking a drive letter.
  53. let dir = empty($SYSTEMDRIVE) ? dir : '/'.tolower($SYSTEMDRIVE[0]).(dir)
  54. if !isdirectory(dir)
  55. if !a:silent
  56. call s:msg_error("invalid directory: '".a:dir."'")
  57. endif
  58. return ''
  59. endif
  60. endif
  61. return dir
  62. endf
  63. func! s:parent_dir(f) abort
  64. let f_noslash = substitute(a:f, escape(s:sep == '\'?'[/\]':'/','\').'\+$', '', 'g')
  65. return s:fix_dir(fnamemodify(f_noslash, ':h'), 0)
  66. endf
  67. if v:version > 704 || v:version == 704 && has('patch279')
  68. func! s:globlist(dir_esc, pat) abort
  69. return globpath(a:dir_esc, a:pat, !s:suf(), 1)
  70. endf
  71. else " Older versions cannot handle filenames containing newlines.
  72. func! s:globlist(dir_esc, pat) abort
  73. return split(globpath(a:dir_esc, a:pat, !s:suf()), "\n")
  74. endf
  75. endif
  76. func! s:list_dir(dir) abort
  77. let s:rel = get(g:, 'dirvish_relative_paths', 0)
  78. " Escape for globpath().
  79. let dir_esc = escape(substitute(a:dir,'\[','[[]','g'), ',;*?{}^$\')
  80. let paths = s:globlist(dir_esc, '*')
  81. "Append dot-prefixed files. globpath() cannot do both in 1 pass.
  82. let paths = paths + s:globlist(dir_esc, '.[^.]*')
  83. if s:rel && !s:eq(a:dir, s:parent_dir(s:sl(getcwd()))) " Avoid blank CWD.
  84. return map(paths, "fnamemodify(v:val, ':p:.')")
  85. else
  86. return map(paths, "fnamemodify(v:val, ':p')")
  87. endif
  88. endf
  89. func! s:info(paths, dirsize) abort
  90. for f in a:paths
  91. " Slash decides how getftype() classifies directory symlinks. #138
  92. let noslash = substitute(f, escape(s:sep,'\').'$', '', 'g')
  93. let fname = len(a:paths) < 2 ? '' : printf('%12.12s ',fnamemodify(substitute(f,'[\\/]\+$','',''),':t'))
  94. let size = (-1 != getfsize(f) && a:dirsize ? matchstr(system('du -hs '.shellescape(f)),'\S\+') : printf('%.2f',getfsize(f)/1000.0).'K')
  95. echo (-1 == getfsize(f) ? '?' : (fname.(getftype(noslash)[0]).' '.getfperm(f)
  96. \.' '.strftime('%Y-%m-%d.%H:%M:%S',getftime(f)).' '.size).('link'!=#getftype(noslash)?'':' -> '.fnamemodify(resolve(f),':~:.')))
  97. endfor
  98. endf
  99. func! s:set_args(args) abort
  100. if exists('*arglistid') && arglistid() == 0
  101. arglocal
  102. endif
  103. let normalized_argv = map(argv(), 'fnamemodify(v:val, ":p")')
  104. for f in a:args
  105. let i = index(normalized_argv, f)
  106. if -1 == i
  107. exe '$argadd '.fnameescape(fnamemodify(f, ':p'))
  108. elseif 1 == len(a:args)
  109. exe (i+1).'argdelete'
  110. syntax clear DirvishArg
  111. endif
  112. endfor
  113. echo 'arglist: '.argc().' files'
  114. " Define (again) DirvishArg syntax group.
  115. exe 'source '.fnameescape(s:srcdir.'/syntax/dirvish.vim')
  116. endf
  117. func! dirvish#shdo(paths, cmd) abort
  118. " Remove empty/duplicate lines.
  119. let lines = uniq(sort(filter(copy(a:paths), '-1!=match(v:val,"\\S")')))
  120. let head = fnamemodify(get(lines, 0, '')[:-2], ':h')
  121. let jagged = 0 != len(filter(copy(lines), 'head != fnamemodify(v:val[:-2], ":h")'))
  122. if empty(lines) | call s:msg_error('Shdo: no files') | return | endif
  123. let dirvish_bufnr = bufnr('%')
  124. let cmd = a:cmd =~# '\V{}' ? a:cmd : (empty(a:cmd)?'{}':(a:cmd.' {}')) "DWIM
  125. " Paths from argv() or non-dirvish buffers may be jagged; assume CWD then.
  126. let dir = jagged ? getcwd() : head
  127. let tmpfile = tempname().(&sh=~?'cmd.exe'?'.bat':(&sh=~'\(powershell\|pwsh\)'?'.ps1':'.sh'))
  128. for i in range(0, len(lines)-1)
  129. let f = substitute(lines[i], escape(s:sep,'\').'$', '', 'g') "trim slash
  130. if !filereadable(f) && !isdirectory(f)
  131. let lines[i] = '#invalid path: '.shellescape(f)
  132. continue
  133. endif
  134. let f = !jagged && 2==exists(':lcd') ? fnamemodify(f, ':t') : lines[i]
  135. let lines[i] = substitute(cmd, '\V{}', escape(shellescape(f),'&\'), 'g')
  136. endfor
  137. execute 'silent split' tmpfile '|' (2==exists(':lcd')?('lcd '.dir):'')
  138. setlocal bufhidden=wipe
  139. silent keepmarks keepjumps call setline(1, lines)
  140. silent write
  141. if executable('chmod')
  142. call system('chmod u+x '.tmpfile)
  143. silent edit
  144. endif
  145. augroup dirvish_shcmd
  146. autocmd! * <buffer>
  147. " Refresh Dirvish after executing a shell command.
  148. exe 'autocmd ShellCmdPost <buffer> nested if !v:shell_error && bufexists('.dirvish_bufnr.')'
  149. \.'|setlocal bufhidden=hide|buffer '.dirvish_bufnr.'|silent! Dirvish'
  150. \.'|buffer '.bufnr('%').'|setlocal bufhidden=wipe|endif'
  151. augroup END
  152. nnoremap <buffer><silent> Z! :silent write<Bar>exe '!'.(has('win32')?fnameescape(escape(expand('%:p:gs?\\?/?'), '&\')):join(map(split(&shell), 'shellescape(v:val)')).' %')<Bar>if !v:shell_error<Bar>close<Bar>endif<CR>
  153. endf
  154. " Returns true if the buffer was modified by the user.
  155. func! s:buf_modified() abort
  156. return b:changedtick > get(b:dirvish, '_c', b:changedtick)
  157. endf
  158. func! s:buf_init() abort
  159. augroup dirvish_buflocal
  160. autocmd! * <buffer>
  161. autocmd BufEnter,WinEnter <buffer> call <SID>on_bufenter()
  162. if exists('##TextChanged')
  163. autocmd TextChanged,TextChangedI <buffer> if <SID>buf_modified()
  164. \&& has('conceal')|exe 'setlocal conceallevel=0'|endif
  165. endif
  166. " BufUnload is fired for :bwipeout/:bdelete/:bunload, _even_ if
  167. " 'nobuflisted'. BufDelete is _not_ fired if 'nobuflisted'.
  168. " NOTE: For 'nohidden' we cannot reliably handle :bdelete like this.
  169. if &hidden
  170. autocmd BufUnload <buffer> call s:on_bufunload()
  171. endif
  172. augroup END
  173. setlocal buftype=nofile noswapfile
  174. endf
  175. func! s:on_bufenter() abort
  176. if bufname('%') is '' " Something is very wrong. #136
  177. return
  178. elseif !exists('b:dirvish') || (empty(getline(1)) && 1 == line('$'))
  179. Dirvish
  180. elseif 3 != &l:conceallevel && !s:buf_modified()
  181. call s:win_init()
  182. else
  183. " Ensure w:dirvish for window splits, `:b <nr>`, etc.
  184. let w:dirvish = extend(get(w:, 'dirvish', {}), b:dirvish, 'keep')
  185. endif
  186. endf
  187. func! s:save_state(d) abort
  188. " Remember previous ('original') buffer.
  189. let p = s:buf_valid(bufnr('%')) || !exists('w:dirvish') ? 0+bufnr('%') : w:dirvish.prevbuf
  190. if !s:buf_valid(p)
  191. "If reached via :edit/:buffer/etc. we cannot get the (former) altbuf.
  192. let p = exists('b:dirvish') && s:buf_valid(b:dirvish.prevbuf) ? b:dirvish.prevbuf : bufnr('#')
  193. endif
  194. " Remember alternate buffer.
  195. let a = (p != bufnr('#') && s:buf_valid(bufnr('#'))) || !exists('w:dirvish') ? 0+bufnr('#') : w:dirvish.altbuf
  196. if !s:buf_valid(a) || a == p
  197. let a = exists('b:dirvish') && s:buf_valid(b:dirvish.altbuf) ? b:dirvish.altbuf : -1
  198. endif
  199. " Save window-local settings.
  200. let a:d.altbuf = a
  201. let a:d.prevbuf = p
  202. let w:dirvish = extend(get(w:, 'dirvish', {}), a:d, 'force')
  203. let [w:dirvish._w_wrap, w:dirvish._w_cul] = [&l:wrap, &l:cul]
  204. if has('conceal') && !exists('b:dirvish')
  205. let [w:dirvish._w_cocu, w:dirvish._w_cole] = [&l:concealcursor, &l:conceallevel]
  206. endif
  207. call s:log(printf('save_state: bufnr=%d altbuf=%d prevbuf=%d', bufnr(''), a:d.altbuf, a:d.prevbuf))
  208. endf
  209. func! s:win_init() abort
  210. let w:dirvish = extend(get(w:, 'dirvish', {}), b:dirvish, 'keep')
  211. setlocal nowrap cursorline
  212. if has('conceal')
  213. setlocal concealcursor=nvc conceallevel=2
  214. endif
  215. endf
  216. func! s:on_bufunload() abort
  217. call s:restore_winlocal_settings()
  218. endf
  219. func! s:buf_close() abort
  220. let d = get(w:, 'dirvish', {})
  221. if empty(d)
  222. return
  223. endif
  224. let [altbuf, prevbuf] = [get(d, 'altbuf', 0), get(d, 'prevbuf', 0)]
  225. call s:log(printf('buf_close: bufnr=%d altbuf=%d prevbuf=%d', bufnr(''), altbuf, prevbuf))
  226. let found_alt = s:try_visit(altbuf, 0)
  227. if !s:try_visit(prevbuf, 0) && !found_alt
  228. \ && (1 == bufnr('%') || (prevbuf != bufnr('%') && altbuf != bufnr('%')))
  229. bdelete
  230. endif
  231. endf
  232. func! s:restore_winlocal_settings() abort
  233. if !exists('w:dirvish') " can happen during VimLeave, etc.
  234. return
  235. endif
  236. if has('conceal') && has_key(w:dirvish, '_w_cocu')
  237. let [&l:cocu, &l:cole] = [w:dirvish._w_cocu, w:dirvish._w_cole]
  238. endif
  239. endf
  240. func! s:open_selected(splitcmd, bg, line1, line2) abort
  241. let curbuf = bufnr('%')
  242. let [curtab, curwin, wincount] = [tabpagenr(), winnr(), winnr('$')]
  243. let p = (a:splitcmd ==# 'p') " Preview-mode
  244. let paths = getline(a:line1, a:line2)
  245. for path in paths
  246. let isdir = path[-1:] == s:sep
  247. if !isdirectory(path) && !filereadable(path)
  248. call s:msg_error(printf('invalid (access denied?): %s', path))
  249. continue
  250. endif
  251. " Open files (not dirs) using relative paths.
  252. let shortname = fnamemodify(path, isdir ? ':p:~' : ':~:.')
  253. if p " Go to previous window.
  254. exe (winnr('$') > 1 ? 'wincmd p|if winnr()=='.winnr().'|wincmd w|endif' : 'vsplit')
  255. endif
  256. if isdir
  257. exe (p || a:splitcmd ==# 'edit' ? '' : a:splitcmd.'|') 'Dirvish' fnameescape(shortname)
  258. else
  259. exe (p ? 'edit' : a:splitcmd) fnameescape(shortname)
  260. endif
  261. " Return to previous window after _each_ split, else we get lost.
  262. if a:bg && (p || (a:splitcmd =~# 'sp' && winnr('$') > wincount))
  263. wincmd p
  264. endif
  265. endfor
  266. if a:bg "return to dirvish buffer
  267. if a:splitcmd ==# 'tabedit'
  268. exe 'tabnext' curtab '|' curwin.'wincmd w'
  269. elseif a:splitcmd ==# 'edit'
  270. execute 'silent keepalt keepjumps buffer' curbuf
  271. endif
  272. elseif !exists('b:dirvish') && exists('w:dirvish')
  273. call s:set_altbuf(w:dirvish.prevbuf)
  274. endif
  275. endf
  276. func! s:set_altbuf(bnr) abort
  277. if !s:buf_valid(a:bnr) | return | endif
  278. if has('patch-7.4.605') | let @# = a:bnr | return | endif
  279. let curbuf = bufnr('%')
  280. if s:try_visit(a:bnr, 1)
  281. let noau = bufloaded(curbuf) ? 'noau' : ''
  282. " Return to the current buffer.
  283. execute 'silent keepjumps' noau s:noswapfile 'buffer' curbuf
  284. endif
  285. endf
  286. func! s:try_visit(bnr, noau) abort
  287. if s:buf_valid(a:bnr)
  288. " If _previous_ buffer is _not_ loaded (because of 'nohidden'), we must
  289. " allow autocmds (else no syntax highlighting; #13).
  290. let noau = a:noau && bufloaded(a:bnr) ? 'noau' : ''
  291. execute 'silent keepjumps' noau s:noswapfile 'buffer' a:bnr
  292. return 1
  293. endif
  294. return 0
  295. endf
  296. if exists('*win_execute')
  297. " Performs `cmd` in all windows showing `bnr`.
  298. func! s:bufwin_do(cmd, bnr) abort
  299. call map(filter(getwininfo(), {_,v -> a:bnr ==# v.bufnr}), {_,v -> win_execute(v.winid, s:noau.' '.a:cmd)})
  300. endf
  301. else
  302. func! s:tab_win_do(tnr, cmd, bnr) abort
  303. exe s:noau 'tabnext' a:tnr
  304. for wnr in range(1, tabpagewinnr(a:tnr, '$'))
  305. if a:bnr ==# winbufnr(wnr)
  306. exe s:noau wnr.'wincmd w'
  307. exe a:cmd
  308. endif
  309. endfor
  310. endf
  311. func! s:bufwin_do(cmd, bnr) abort
  312. let [curtab, curwin, curwinalt, curheight, curwidth, squashcmds] = [tabpagenr(), winnr(), winnr('#'), winheight(0), winwidth(0), filter(split(winrestcmd(), '|'), 'v:val =~# " 0$"')]
  313. for tnr in range(1, tabpagenr('$'))
  314. let [origwin, origwinalt] = [tabpagewinnr(tnr), tabpagewinnr(tnr, '#')]
  315. for bnr in tabpagebuflist(tnr)
  316. if a:bnr == bnr
  317. call s:tab_win_do(tnr, a:cmd, a:bnr)
  318. exe s:noau origwinalt.'wincmd w|' s:noau origwin.'wincmd w'
  319. break
  320. endif
  321. endfor
  322. endfor
  323. exe s:noau 'tabnext '.curtab
  324. exe s:noau curwinalt.'wincmd w|' s:noau curwin.'wincmd w'
  325. if (&winminheight == 0 && curheight != winheight(0)) || (&winminwidth == 0 && curwidth != winwidth(0))
  326. for squashcmd in squashcmds
  327. if squashcmd =~# '^\Cvert ' && winwidth(matchstr('\d\+', squashcmd)) != 0
  328. \ || squashcmd =~# '^\d' && winheight(matchstr('\d\+', squashcmd)) != 0
  329. exe s:noau squashcmd
  330. endif
  331. endfor
  332. endif
  333. endf
  334. endif
  335. func! s:buf_render(dir, lastpath) abort
  336. let bnr = bufnr('%')
  337. let isnew = empty(getline(1))
  338. if !isdirectory(a:dir)
  339. echoerr 'dirvish: not a directory:' a:dir
  340. return
  341. endif
  342. if !isnew
  343. call s:bufwin_do('let w:dirvish["_view"] = winsaveview()', bnr)
  344. endif
  345. if v:version > 704 || v:version == 704 && has("patch73")
  346. setlocal undolevels=-1
  347. endif
  348. silent keepmarks keepjumps %delete _
  349. silent keepmarks keepjumps call setline(1, s:list_dir(a:dir))
  350. if type("") == type(get(g:, 'dirvish_mode')) " Apply user's filter.
  351. execute get(g:, 'dirvish_mode')
  352. endif
  353. if v:version > 704 || v:version == 704 && has("patch73")
  354. setlocal undolevels<
  355. endif
  356. if !isnew
  357. call s:bufwin_do('call winrestview(w:dirvish["_view"])', bnr)
  358. endif
  359. if !empty(a:lastpath)
  360. let pat = tr(s:f(a:lastpath), '/', s:sep) " platform slashes
  361. call search('\V\^'.escape(pat, '\').'\$', 'cw')
  362. endif
  363. " Place cursor on the tail (last path segment).
  364. call search('\'.s:sep.'\zs[^\'.s:sep.']\+\'.s:sep.'\?$', 'c', line('.'))
  365. " TRICK: From :help getfperm(): "If the directory cannot be read, empty string is returned."
  366. " This misses the "--x" case (no "r" access), but it's the best we have.
  367. if empty(getline(1)) && '' ==# getfperm(a:dir . '/.')
  368. call s:msg_error(printf('cannot list directory (permissions: %s): %s', getfperm(a:dir), a:dir))
  369. endif
  370. endf
  371. func! s:apply_icons() abort
  372. if 0 == len(s:cb_map)
  373. return
  374. endif
  375. highlight clear Conceal
  376. let i = 0
  377. for f in getline(1, '$')
  378. let i += 1
  379. let icon = ''
  380. for id in sort(keys(s:cb_map))
  381. let icon = s:cb_map[id](f)
  382. if -1 != match(icon, '\S')
  383. break
  384. endif
  385. endfor
  386. if icon != ''
  387. let isdir = (f[-1:] == s:sep)
  388. let f = substitute(s:f(f), escape(s:sep,'\').'$', '', 'g') " Full path, trim slash.
  389. let tail_esc = escape(fnamemodify(f,':t').(isdir?(s:sep):''), '[,*.^$~\')
  390. exe 'syntax match DirvishColumnHead =\%'.i.'l^.\{-}\ze'.tail_esc.'$= conceal cchar='.icon
  391. endif
  392. endfor
  393. endf
  394. let s:recursive = ''
  395. func! s:open_dir(d, reload) abort
  396. if s:recursive ==# a:d._dir
  397. return
  398. endif
  399. let s:recursive = a:d._dir
  400. call s:log(printf('open_dir ENTER: %d %s', bufnr('%'), a:d._dir))
  401. let d = a:d
  402. let dirname_without_sep = substitute(d._dir, '[\\/]\+$', '', 'g')
  403. " Vim tends to 'simplify' buffer names. Examples (gvim 7.4.618):
  404. " ~\foo\, ~\foo, foo\, foo
  405. " Try to find an existing buffer before creating a new one.
  406. let bnr = -1
  407. for pat in ['', ':~:.', ':~']
  408. let dir = fnamemodify(d._dir, pat)
  409. if dir == '' | continue | endif
  410. let bnr = bufnr('^'.dir.'$')
  411. if -1 != bnr
  412. break
  413. endif
  414. endfor
  415. " Note: :noautocmd not used here, to allow BufEnter/BufNew. 61282f2453af
  416. " Thus s:recursive guards against recursion (for performance).
  417. if -1 == bnr
  418. execute 'silent' s:noswapfile 'keepalt edit' fnameescape(d._dir)
  419. else
  420. execute 'silent' s:noswapfile 'buffer' bnr
  421. endif
  422. " Force a normalized directory path.
  423. " - Starts with "~/" or "/", ie absolute (important for ":h").
  424. " - Ends with "/".
  425. " - Avoids ".././..", ".", "./", etc. (breaks %:p, not updated on :cd).
  426. " - Avoids [Scratch] in some cases (":e ~/" on Windows).
  427. if bufname('%')[-1:] != '/' || bufname('%')[0:1] !=# d._dir[0:1]
  428. execute 'silent' s:noswapfile 'file' fnameescape(d._dir)
  429. endif
  430. if !isdirectory(bufname('%')) " sanity check
  431. throw 'invalid directory: '.bufname('%')
  432. endif
  433. if &buflisted && bufnr('$') > 1
  434. setlocal nobuflisted
  435. endif
  436. call s:set_altbuf(d.prevbuf) "in case of :bd, :read#, etc.
  437. let b:dirvish = exists('b:dirvish') ? extend(b:dirvish, d, 'force') : d
  438. call s:buf_init()
  439. call s:win_init()
  440. if a:reload || s:should_reload()
  441. call s:buf_render(b:dirvish._dir, get(b:dirvish, 'lastpath', ''))
  442. " Set up Dirvish before any other `FileType dirvish` handler.
  443. exe 'source '.fnameescape(s:srcdir.'/ftplugin/dirvish.vim')
  444. let curwin = winnr()
  445. setlocal filetype=dirvish
  446. if curwin != winnr() | throw 'FileType autocmd changed the window' | endif
  447. let b:dirvish._c = b:changedtick
  448. call s:apply_icons()
  449. endif
  450. let s:recursive = ''
  451. call s:log(printf('open_dir EXIT : %d %s', bufnr('%'), a:d._dir))
  452. endf
  453. func! s:should_reload() abort
  454. return !s:buf_modified() || (empty(getline(1)) && 1 == line('$'))
  455. endf
  456. func! s:buf_valid(bnr) abort
  457. return bufexists(a:bnr) && (empty(bufname(a:bnr)) || !isdirectory(s:sl(bufname(a:bnr))))
  458. endf
  459. func! dirvish#open(...) range abort
  460. if &autochdir
  461. call s:msg_error("'autochdir' is not supported")
  462. return
  463. endif
  464. if (&bufhidden =~# '\vunload|delete|wipe' || (!&autowriteall && !&hidden && &modified))
  465. \ && (!exists("*win_findbuf") || len(win_findbuf(winbufnr(0))) == 1)
  466. call s:msg_error(&modified ? 'E37: No write since last change' : 'E37: Buffer would be deleted: '.bufnr('%'))
  467. return
  468. endif
  469. if a:0 > 1
  470. " Detect whether a <Cmd> mapping or the legacy fallback is being used
  471. let l:visual_lines = mode(0) == 'n' ? [a:firstline, a:lastline] : [line('v'), line('.')]
  472. call s:open_selected(a:1, a:2, min(l:visual_lines), max(l:visual_lines))
  473. return
  474. endif
  475. let d = {}
  476. let is_uri = -1 != match(a:1, '^\w\+:[\/][\/]')
  477. let from_path = s:sl(fnamemodify(bufname('%'), ':p'))
  478. let to_path = s:sl(fnamemodify(!empty(a:1) || empty(@%) ? a:1 : @%, ':p'))
  479. let d._dir = s:fix_dir(filereadable(to_path) ? fnamemodify(to_path, ':p:h') : to_path, is_uri)
  480. " Fallback to CWD for URIs. #127
  481. let d._dir = empty(d._dir) && is_uri ? s:fix_dir(getcwd(), is_uri) : d._dir
  482. if empty(d._dir) " s:fix_dir() already showed error.
  483. return
  484. endif
  485. let reloading = exists('b:dirvish') && d._dir ==# b:dirvish._dir && s:recursive !=# d._dir
  486. if reloading
  487. let d.lastpath = '' " Do not place cursor when reloading.
  488. elseif !is_uri && s:eq(d._dir, s:parent_dir(from_path))
  489. let d.lastpath = from_path " Save lastpath when navigating _up_.
  490. endif
  491. call s:save_state(d)
  492. call s:open_dir(d, reloading)
  493. endf
  494. func! dirvish#add_icon_fn(fn) abort
  495. if !exists('v:t_func') || type(a:fn) != v:t_func | throw 'argument must be a Funcref' | endif
  496. let s:cb_map[string(a:fn)] = a:fn
  497. return string(a:fn)
  498. endf
  499. func! dirvish#remove_icon_fn(fn_id) abort
  500. if has_key(s:cb_map, a:fn_id)
  501. call remove(s:cb_map, a:fn_id)
  502. return 1
  503. endif
  504. return 0
  505. endf
  506. nnoremap <silent> <Plug>(dirvish_quit) :<C-U>call <SID>buf_close()<CR>
  507. nnoremap <silent> <Plug>(dirvish_arg) :<C-U>call <SID>set_args([getline('.')])<CR>
  508. xnoremap <silent> <Plug>(dirvish_arg) :<C-U>call <SID>set_args(getline("'<", "'>"))<CR>
  509. nnoremap <silent> <Plug>(dirvish_K) :<C-U>call <SID>info([getline('.')],!!v:count)<CR>
  510. xnoremap <silent> <Plug>(dirvish_K) :<C-U>call <SID>info(getline("'<", "'>"),!!v:count)<CR>