mirror of
https://github.com/inkarkat/vim-ingo-library.git
synced 2026-06-10 15:37:43 +02:00
a2ff39e7b5
Define custom command to complete files from a specified directory.
445 lines
19 KiB
VimL
445 lines
19 KiB
VimL
" CommandCompleteDirForAction.vim: Define custom command to complete files from a specified directory.
|
|
"
|
|
" DESCRIPTION:
|
|
" In GVIM, one can define a menu item which uses browse() in combination with
|
|
" an Ex command to open a file browser dialog in a particular directory, lets
|
|
" the user select a file, and then uses that file for a predefined Ex command.
|
|
" This script provides a function to define similar custom commands for use
|
|
" without a GUI file selector, relying instead on custom command completion.
|
|
"
|
|
" EXAMPLE:
|
|
" Define a command :BrowseTemp that edits a text file from the system TEMP
|
|
" directory. >
|
|
" call CommandCompleteDirForAction#setup(
|
|
" \ '',
|
|
" \ 'BrowseTemp',
|
|
" \ 'edit',
|
|
" \ (exists('$TEMP') ? $TEMP : '/tmp'),
|
|
" \ '*.txt',
|
|
" \ '',
|
|
" \ ''
|
|
" \)
|
|
" You can then use the new command with file completion:
|
|
" :BrowseTemp f<Tab> -> :BrowseTemp foo.txt
|
|
"
|
|
" INSTALLATION:
|
|
" Put the script into your user or system Vim autoload directory (e.g.
|
|
" ~/.vim/autoload).
|
|
|
|
" DEPENDENCIES:
|
|
" - ingo/cmdargs/file.vim autoload script
|
|
" - ingo/compat.vim autoload script
|
|
" - ingo/err.vim autoload script
|
|
" - ingo/fs/path.vim autoload script
|
|
" - ingo/list.vim autoload script
|
|
" - ingo/msg.vim autoload script
|
|
|
|
" Copyright: (C) 2009-2013 Ingo Karkat
|
|
" The VIM LICENSE applies to this script; see ':help copyright'.
|
|
"
|
|
" Maintainer: Ingo Karkat <ingo@karkat.de>
|
|
"
|
|
" REVISION DATE REMARKS
|
|
" 024 13-Feb-2015 ENH: Support List of a:parameters.browsefilter.
|
|
" 023 08-Jan-2015 ENH: Support multiple and fnamemodify()'ed
|
|
" placement of the filespec in a:parameters.action
|
|
" similar to 'makeprg'.
|
|
" 022 23-Sep-2014 Fix typo.
|
|
" 021 22-Sep-2014 Use ingo#compat#glob().
|
|
" 020 07-Jun-2014 Abort on error.
|
|
" 019 08-Aug-2013 Move escapings.vim into ingo-library.
|
|
" 018 26-Jun-2013 Use ingo/fs/path.vim.
|
|
" 017 10-Jun-2013 Better handling for errors from :echoerr.
|
|
" 016 01-Jun-2013 Move ingofileargs.vim into ingo-library.
|
|
" 015 31-May-2013 Minor refactoring.
|
|
" 014 23-Mar-2013 ENH: Allow determining the a:dirspec during
|
|
" runtime by taking a Funcref instead of string.
|
|
" Use error handling functions from ingo/msg.vim.
|
|
" 013 28-Jan-2013 Handle ++enc= and +cmd file options and
|
|
" commands. This requires an extension of the
|
|
" a:parameters.action,
|
|
" a:parameters.FilenameProcessingFunction and
|
|
" a:parameters.FilespecProcessingFunction values
|
|
" to take and return an additional
|
|
" fileOptionsAndCommands argument.
|
|
" 012 28-Dec-2012 Apply special logic to support lnum 0 with
|
|
" a:parameters.commandAttributes = "-range=-1"
|
|
" described at :help ingo-command-lnum.
|
|
" 011 13-Sep-2012 ENH: Added
|
|
" a:parameters.FilespecProcessingFunction to allow
|
|
" processing of both dirspec and filename.
|
|
" This is now used by the :Vim command to expand
|
|
" arbitrary <SID> numbers to the corresponding
|
|
" full path.
|
|
" FIX: Actually abort the processing when
|
|
" a:parameters.FilenameProcessingFunction returns
|
|
" an empty filename (as was documented).
|
|
" 010 14-May-2012 ENH: Allow special "%" value or Funcref for
|
|
" a:parameters.defaultFilename.
|
|
" FIX: Don't append <bang> to a:Action of type
|
|
" command; this should be contained in a:Action =
|
|
" "MyCommand<bang>".
|
|
" Handle custom exceptions thrown from Funcrefs.
|
|
" 009 27-Jan-2012 ENH: Get <bang> information into s:Command() and
|
|
" pass this on to a:Action, and make this
|
|
" accessible to a:Action Funcrefs via a context
|
|
" object g:CommandCompleteDirForAction_Context.
|
|
" 008 21-Sep-2011 ENH: action and postAction now also support
|
|
" Funcrefs instead of Ex commands.
|
|
" Generated command now actually demands argument
|
|
" unless a:parameters.defaultFilename is given.
|
|
" a:parameters.defaultFilename can be empty,
|
|
" resulting in a command with optional argument
|
|
" and no filename passed to a:parameters.action;
|
|
" the action (probably a Funcref) is supposed to
|
|
" handle this. Beforehand, the command would be
|
|
" aborted if the filename was empty.
|
|
" 007 22-Jan-2011 Collapsed s:CommandWithOptionalArgument(),
|
|
" s:CommandWithPostAction() and the direct
|
|
" definition for a non-optional, non-postAction
|
|
" command into s:Command(), which already handles
|
|
" all cases anyway, getting rid of the
|
|
" conditional.
|
|
" ENH: Added
|
|
" a:parameters.FilenameProcessingFunction to allow
|
|
" processing of the completed or typed filespec.
|
|
" This is used by the :Vim command to correct the
|
|
" .vimrc to ../.vimrc when the name is fully
|
|
" typed, not completed.
|
|
" 006 10-Dec-2010 ENH: Added a:parameters.overrideCompleteFunction
|
|
" and returning the generated completion function
|
|
" name in order to allow hooking into the
|
|
" completion. This is used by the :Vim command to
|
|
" also offer .vimrc and .gvimrc completion
|
|
" candidates.
|
|
" 005 27-Aug-2010 FIX: Filtering out subdirectories from the file
|
|
" completion candidates.
|
|
" ENH: Added a:parameters.isIncludeSubdirs flag to
|
|
" allow inclusion of subdirectories. Made this
|
|
" work even when a browsefilter is set.
|
|
" 004 06-Jul-2010 Simplified CommandCompleteDirForAction#setup()
|
|
" interface via parameter hash that allows to omit
|
|
" defaults and makes it more easy to extend.
|
|
" Implemented a:parameters.postAction, e.g. to
|
|
" :setfiletype after opening the file.
|
|
" 003 27-Oct-2009 BUG: With optional argument, the a:filename
|
|
" passed to s:CommandWithOptionalArgument() must
|
|
" not be escaped, only all other filespec
|
|
" fragments.
|
|
" 002 26-Oct-2009 Added to arguments: a:commandAttributes e.g. to
|
|
" make buffer-local commands, a:defaultFilename to
|
|
" make the filename argument optional.
|
|
" 001 26-Oct-2009 file creation
|
|
let s:save_cpo = &cpo
|
|
set cpo&vim
|
|
|
|
function! s:RemoveDirspec( filespec, dirspecs )
|
|
for l:dirspec in a:dirspecs
|
|
if strpart(a:filespec, 0, strlen(l:dirspec)) ==# l:dirspec
|
|
return strpart(a:filespec, strlen(l:dirspec))
|
|
endif
|
|
endfor
|
|
return a:filespec
|
|
endfunction
|
|
function! s:CompleteFiles( dirspec, browsefilter, wildignore, isIncludeSubdirs, argLead )
|
|
try
|
|
let l:dirspec = (type(a:dirspec) == 2 ? call(a:dirspec, []) : a:dirspec)
|
|
catch /^Vim\%((\a\+)\)\=:E/
|
|
throw ingo#msg#MsgFromVimException() " Don't swallow Vimscript errors.
|
|
catch /^Vim\%((\a\+)\)\=:/
|
|
call ingo#msg#VimExceptionMsg() " Errors from :echoerr.
|
|
catch
|
|
call ingo#msg#ErrorMsg(v:exception)
|
|
sleep 1 " Otherwise, the error isn't visible from inside the command-line completion function.
|
|
return []
|
|
endtry
|
|
|
|
let l:browsefilter = (empty(a:browsefilter) ? ['*'] : ingo#list#Make(a:browsefilter))
|
|
let l:save_wildignore = &wildignore
|
|
if type(a:wildignore) == type('')
|
|
let &wildignore = a:wildignore
|
|
endif
|
|
try
|
|
let l:filespecs = []
|
|
let l:sourceCnt = 0
|
|
|
|
if a:isIncludeSubdirs
|
|
" If the l:dirspec itself contains wildcards, there may be multiple
|
|
" matches.
|
|
let l:resolvedDirspecs = ingo#compat#glob(l:dirspec, 0, 1)
|
|
|
|
" If there is a browsefilter, we need to add all directories
|
|
" separately, as most of them probably have been filtered away by
|
|
" the (file-based) a:browsefilter.
|
|
if ! empty(a:browsefilter)
|
|
let l:dirspecWildcard = l:dirspec . a:argLead . '*' . ingo#fs#path#Separator()
|
|
let l:filespecs += ingo#compat#glob(l:dirspecWildcard, 0, 1)
|
|
let l:sourceCnt += 1
|
|
endif
|
|
endif
|
|
|
|
for l:filter in l:browsefilter
|
|
let l:filespecWildcard = l:dirspec . a:argLead . l:filter
|
|
let l:filespecs += ingo#compat#glob(l:filespecWildcard, 0, 1)
|
|
let l:sourceCnt += 1
|
|
endfor
|
|
|
|
if a:isIncludeSubdirs
|
|
if empty(a:browsefilter)
|
|
" glob() doesn't add a trailing path separator on directories
|
|
" unless the glob pattern has one at the end. Append the path
|
|
" separator here to be consistent with the alternative block
|
|
" above, the built-in completion, and because it makes sense to
|
|
" show the path separator, because then autocompletion of the
|
|
" directory contents can quickly be continued.
|
|
call map(l:filespecs, 'isdirectory(v:val) ? v:val . ingo#fs#path#Separator() : v:val')
|
|
endif
|
|
|
|
call map(
|
|
\ l:filespecs,
|
|
\ 'ingo#compat#fnameescape(s:RemoveDirspec(v:val, l:resolvedDirspecs))'
|
|
\)
|
|
else
|
|
call map(
|
|
\ filter(
|
|
\ l:filespecs,
|
|
\ '! isdirectory(v:val)'
|
|
\ ),
|
|
\ 'ingo#compat#fnameescape(fnamemodify(v:val, ":t"))'
|
|
\)
|
|
endif
|
|
|
|
if l:sourceCnt > 1
|
|
return ingo#compat#uniq(sort(l:filespecs), 's:SuffixesSort') " Maintain lower priority of 'suffixes' while sorting.
|
|
else
|
|
return l:filespecs
|
|
endif
|
|
finally
|
|
let &wildignore = l:save_wildignore
|
|
endtry
|
|
endfunction
|
|
function! s:SuffixesSort( f1, f2 )
|
|
" TODO
|
|
return 0
|
|
endfunction
|
|
|
|
function! s:Command( isBang, Action, PostAction, DefaultFilename, FilenameProcessingFunction, FilespecProcessingFunction, dirspec, filename )
|
|
try
|
|
"****Dechomsg '****' a:isBang string(a:Action) string(a:PostAction) string(a:DefaultFilename) string(a:FilenameProcessingFunction) string(a:FilespecProcessingFunction) string(a:dirspec) string(a:filename)
|
|
let l:dirspec = (type(a:dirspec) == 2 ? call(a:dirspec, []) : a:dirspec)
|
|
|
|
" Detach any file options or commands for assembling the filespec.
|
|
let [l:fileOptionsAndCommands, l:filename] = ingo#cmdargs#file#FilterEscapedFileOptionsAndCommands(a:filename)
|
|
"****D echomsg '****' string(l:filename) string(l:fileOptionsAndCommands)
|
|
" Set up a context object so that Funcrefs can have access to the
|
|
" information whether <bang> was given.
|
|
let g:CommandCompleteDirForAction_Context = { 'bang': a:isBang }
|
|
|
|
" l:filename comes from the custom command, and must be taken as is (the
|
|
" custom completion will have already escaped the completion).
|
|
" All other filespec fragments still need escaping.
|
|
|
|
if empty(l:filename)
|
|
if type(a:DefaultFilename) == 2
|
|
let l:unescapedFilename = call(a:DefaultFilename, [l:dirspec])
|
|
elseif a:DefaultFilename ==# '%'
|
|
let l:unescapedFilename = expand('%:t')
|
|
else
|
|
let l:unescapedFilename = a:DefaultFilename
|
|
endif
|
|
let l:filename = ingo#compat#fnameescape(l:unescapedFilename)
|
|
endif
|
|
|
|
if ! empty(a:FilenameProcessingFunction)
|
|
let l:processedFilename = call(a:FilenameProcessingFunction, [l:filename, l:fileOptionsAndCommands])
|
|
if empty(l:processedFilename) || empty(l:processedFilename[0])
|
|
return 1
|
|
else
|
|
let [l:filename, l:fileOptionsAndCommands] = l:processedFilename
|
|
endif
|
|
endif
|
|
if ! empty(a:FilespecProcessingFunction)
|
|
let l:processedFilespec = call(a:FilespecProcessingFunction, [l:dirspec, l:filename, l:fileOptionsAndCommands])
|
|
if empty(l:processedFilespec) || empty(join(l:processedFilespec[0:1], ''))
|
|
return 1
|
|
else
|
|
let [l:dirspec, l:filename, l:fileOptionsAndCommands] = l:processedFilespec
|
|
endif
|
|
endif
|
|
|
|
let l:expandExpr = '\%(^\|\s\zs\|"\)%\%(:\S\+\)\?\%("\|\ze\s\|$\)'
|
|
if type(a:Action) == 2
|
|
call call(a:Action, [ingo#compat#fnameescape(l:dirspec), l:filename, l:fileOptionsAndCommands])
|
|
elseif a:Action =~# l:expandExpr
|
|
" Similar to 'makeprg', the location of the inserted filespec can be
|
|
" controlled via "%".
|
|
let l:escapedFilespec = ingo#compat#fnameescape(l:dirspec) . l:filename
|
|
let l:unescapedFilespec = l:dirspec . ingo#escape#file#fnameunescape(l:filename)
|
|
let l:action = substitute(a:Action, l:expandExpr, '\=s:Expand(submatch(0), l:fileOptionsAndCommands, l:escapedFilespec, l:unescapedFilespec)', 'g')
|
|
execute l:action
|
|
else
|
|
execute a:Action l:fileOptionsAndCommands . ingo#compat#fnameescape(l:dirspec) . l:filename
|
|
endif
|
|
|
|
if ! empty(a:PostAction)
|
|
if type(a:PostAction) == 2
|
|
call call(a:PostAction, [])
|
|
else
|
|
execute a:PostAction
|
|
endif
|
|
endif
|
|
return 1
|
|
catch /^Vim\%((\a\+)\)\=:/
|
|
call ingo#err#SetVimException()
|
|
return 0
|
|
catch
|
|
call ingo#err#Set(v:exception)
|
|
return 0
|
|
finally
|
|
unlet! g:CommandCompleteDirForAction_Context
|
|
endtry
|
|
endfunction
|
|
function! s:Expand( expr, fileOptionsAndCommands, escapedFilespec, unescapedFilespec )
|
|
if a:expr ==# '%:+'
|
|
return a:fileOptionsAndCommands
|
|
elseif a:expr ==# '"%"'
|
|
return string(a:unescapedFilespec)
|
|
elseif a:expr =~# '^"%:.*"$'
|
|
return string(fnamemodify(a:unescapedFilespec, a:expr[2:-2]))
|
|
elseif a:expr ==# '%'
|
|
return a:escapedFilespec
|
|
elseif a:expr =~# '^%:.*$'
|
|
return fnamemodify(a:escapedFilespec, a:expr[1:])
|
|
else
|
|
return a:expr
|
|
endif
|
|
endfunction
|
|
|
|
let s:count = 0
|
|
function! CommandCompleteDirForAction#setup( command, dirspec, parameters )
|
|
"*******************************************************************************
|
|
"* PURPOSE:
|
|
" Define a custom a:command that takes an (potentially optional) single file
|
|
" argument and executes the a:parameters.action command or Funcref with it.
|
|
" The command will have a custom completion that completes files from
|
|
" a:dirspec, with a:parameters.browsefilter applied and
|
|
" a:parameters.wildignore extensions filtered out. The custom completion will
|
|
" return the list of file (/ directory / subdir path) names found. Those
|
|
" should be interpreted relative to (and thus do not include) a:dirspec.
|
|
"* ASSUMPTIONS / PRECONDITIONS:
|
|
" None.
|
|
"* EFFECTS / POSTCONDITIONS:
|
|
" Defines custom a:command that takes one filename argument, which will have
|
|
" filename completion from a:dirspec. Unless a:parameters.defaultFilename is
|
|
" provided, the filename argument is mandatory.
|
|
"* INPUTS:
|
|
" a:command Name of the custom command to be defined.
|
|
" a:dirspec Directory (including trailing path separator!) from which
|
|
" files will be completed.
|
|
" Or Funcref to a function that takes no arguments and returns the
|
|
" dirspec.
|
|
"
|
|
" a:parameters.commandAttributes
|
|
" Optional :command {attr}, e.g. <buffer>, -bang, -range.
|
|
" Funcrefs can access the <bang> via
|
|
" g:CommandCompleteDirForAction_Context.bang.
|
|
" a:parameters.action
|
|
" Ex command (e.g. 'edit', '<line1>read') to be invoked with the
|
|
" completed filespec. Default is the :drop / :Drop command.
|
|
" Or Funcref to a function that takes the dirspec, filename (both
|
|
" already escaped for use in an Ex command), and potential
|
|
" fileOptionsAndCommands (e.g. ++enc=latin1 +set\ ft=c) and performs
|
|
" the action itself. Throw an error message if needed.
|
|
" a:parameters.postAction
|
|
" Ex command to be invoked after the file has been opened via
|
|
" a:parameters.action. Default empty.
|
|
" Or Funcref to a function that takes no arguments and performs the
|
|
" post actions itself. Throw an error message if needed.
|
|
" a:parameters.browsefilter
|
|
" File wildcard (e.g. '*.txt') used for filtering the files in
|
|
" a:dirspec. Multiple can be specified as a List. Default is empty
|
|
" string to include all (non-hidden) files. Does not apply to
|
|
" subdirectory names (but applies to the files inside the
|
|
" subdirectories).
|
|
" a:parameters.wildignore
|
|
" Comma-separated list of file extensions to be ignored. This is
|
|
" similar to a:parameters.browsefilter, but with inverted semantics,
|
|
" only file extensions, and multiple possible values. Use empty string
|
|
" to disable and pass 0 (the default) to keep the current global
|
|
" 'wildignore' setting.
|
|
" a:parameters.isIncludeSubdirs
|
|
" Flag whether subdirectories will be included in the completion
|
|
" matches. By default, only files in a:dirspec itself will be offered.
|
|
" a:parameters.defaultFilename
|
|
" If specified, the command will not require the filename argument,
|
|
" and default to this filename if none is specified.
|
|
" The special value "%" will be replaced with the current buffer's
|
|
" filename.
|
|
" Or Funcref to a function that takes the dirspec and returns the
|
|
" filename. Throw an error message if needed.
|
|
" This can resolve to an empty string; however, then your
|
|
" a:parameters.action has to cope with that (e.g. by putting up a
|
|
" browse dialog).
|
|
" a:parameters.overrideCompleteFunction
|
|
" If not empty, will be used as the :command -complete=customlist,...
|
|
" completion function name. This hook can be used to manipulate the
|
|
" completion list. This overriding completion function probably will
|
|
" still invoke the generated custom completion function, which is
|
|
" therefore returned from this setup function.
|
|
" a:parameters.FilenameProcessingFunction
|
|
" If not empty, will be passed the completed (or default) filespec and
|
|
" potential fileOptionsAndCommands, and expects a similar List of
|
|
" [filespec, fileOptionsAndCommands] in return. (Or an empty List,
|
|
" which will abort the command.)
|
|
" a:parameters.FilespecProcessingFunction
|
|
" If not empty, will be passed the (not escaped) dirspec, the
|
|
" completed (or default) filespec, and the potential
|
|
" fileOptionsAndCommands, and expects a similar List of [dirspec,
|
|
" filespec, fileOptionsAndCommands] in return. (Or an empty List,
|
|
" which will abort the command.)
|
|
"
|
|
"* RETURN VALUES:
|
|
" Name of the generated custom completion function.
|
|
"*******************************************************************************
|
|
let l:commandAttributes = get(a:parameters, 'commandAttributes', '')
|
|
let l:Action = get(a:parameters, 'action', ((exists(':Drop') == 2) ? 'Drop' : 'drop'))
|
|
let l:PostAction = get(a:parameters, 'postAction', '')
|
|
let l:browsefilter = get(a:parameters, 'browsefilter', '')
|
|
let l:wildignore = get(a:parameters, 'wildignore', 0)
|
|
let l:isIncludeSubdirs = get(a:parameters, 'isIncludeSubdirs', 0)
|
|
let l:DefaultFilename = get(a:parameters, 'defaultFilename', '')
|
|
let l:FilenameProcessingFunction = get(a:parameters, 'FilenameProcessingFunction', '')
|
|
let l:FilespecProcessingFunction = get(a:parameters, 'FilespecProcessingFunction', '')
|
|
|
|
let s:count += 1
|
|
let l:generatedCompleteFunctionName = 'CompleteDir' . s:count
|
|
let l:completeFunctionName = get(a:parameters, 'overrideCompleteFunction', l:generatedCompleteFunctionName)
|
|
execute
|
|
\ printf("function! %s(ArgLead, CmdLine, CursorPos)\n", l:generatedCompleteFunctionName) .
|
|
\ printf(" return s:CompleteFiles(%s, %s, %s, %d, a:ArgLead)\n",
|
|
\ string(a:dirspec), string(l:browsefilter), string(l:wildignore), l:isIncludeSubdirs
|
|
\ ) . "endfunction"
|
|
|
|
execute printf('command! -bar -nargs=%s -complete=customlist,%s %s %s if ! <SID>Command(<bang>0, %s, %s, %s, %s, %s, %s, <q-args>) | echoerr ingo#err#Get() | endif',
|
|
\ (has_key(a:parameters, 'defaultFilename') ? '?' : '1'),
|
|
\ l:completeFunctionName,
|
|
\ l:commandAttributes,
|
|
\ a:command,
|
|
\ (l:commandAttributes =~# '-range=-1' && l:Action =~# '^<line[12]>,\@!' ?
|
|
\ '(<line2> == 1 ? <line1> : <line2>) . ' . string(l:Action[7:]) :
|
|
\ string(l:Action)
|
|
\ ),
|
|
\ string(l:PostAction),
|
|
\ string(l:DefaultFilename),
|
|
\ string(l:FilenameProcessingFunction),
|
|
\ string(l:FilespecProcessingFunction),
|
|
\ string(a:dirspec),
|
|
\)
|
|
|
|
return l:generatedCompleteFunctionName
|
|
endfunction
|
|
|
|
let &cpo = s:save_cpo
|
|
unlet s:save_cpo
|
|
" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax :
|