"============================================================================= " FILE: rec.vim " AUTHOR: Shougo Matsushita " License: MIT license {{{ " Permission is hereby granted, free of charge, to any person obtaining " a copy of this software and associated documentation files (the " "Software"), to deal in the Software without restriction, including " without limitation the rights to use, copy, modify, merge, publish, " distribute, sublicense, and/or sell copies of the Software, and to " permit persons to whom the Software is furnished to do so, subject to " the following conditions: " " The above copyright notice and this permission notice shall be included " in all copies or substantial portions of the Software. " " THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS " OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF " MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. " IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY " CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, " TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE " SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. " }}} "============================================================================= let s:save_cpo = &cpo set cpo&vim if exists('g:unite_source_rec_async_command') && \ type(g:unite_source_rec_async_command) == type('') call unite#print_error( \ 'g:unite_source_rec_async_command must be list type.') endif " Variables "{{{ call unite#util#set_default( \ 'g:unite_source_rec_min_cache_files', 100, \ 'g:unite_source_file_rec_min_cache_files') call unite#util#set_default( \ 'g:unite_source_rec_max_cache_files', 20000, \ 'g:unite_source_file_rec_max_cache_files') call unite#util#set_default('g:unite_source_rec_unit', \ unite#util#is_windows() ? 1000 : 2000) " -L follows symbolic links to have the same behaviour as file_rec call unite#util#set_default( \ 'g:unite_source_rec_async_command', ( \ !unite#util#is_windows() && executable('find') ? \ ['find', '-L'] : []), \ 'g:unite_source_file_rec_async_command') call unite#util#set_default( \ 'g:unite_source_rec_find_args', \ ['-path', '*/.git/*', '-prune', '-o', '-type', 'l', '-print']) call unite#util#set_default( \ 'g:unite_source_rec_git_command', 'git') "}}} let s:Cache = unite#util#get_vital_cache() let s:continuation = { 'directory' : {}, 'file' : {} } " Source rec. let s:source_file_rec = { \ 'name' : 'file_rec', \ 'description' : 'candidates from directory by recursive', \ 'hooks' : {}, \ 'default_kind' : 'file', \ 'max_candidates' : 50, \ 'ignore_globs' : [ \ '.', '*~', '*.o', '*.exe', '*.bak', \ 'DS_Store', '*.pyc', '*.sw[po]', '*.class', \ '.hg/**', '.git/**', '.bzr/**', '.svn/**', \ 'tags', 'tags-*' \ ], \ 'matchers' : [ 'converter_relative_word', 'matcher_default' ], \ } function! s:source_file_rec.gather_candidates(args, context) abort "{{{ let a:context.source__directory = \ get(s:get_paths(a:args, a:context), 0, '') let directory = a:context.source__directory if directory == '' " Not in project directory. call unite#print_source_message( \ 'Not in project directory.', self.name) let a:context.is_async = 0 return [] endif call unite#print_source_message( \ 'directory: ' . directory, self.name) call s:init_continuation(a:context, directory) let continuation = a:context.source__continuation if empty(continuation.rest) || continuation.end " Disable async. let a:context.is_async = 0 let continuation.end = 1 endif return deepcopy(continuation.files) endfunction"}}} function! s:source_file_rec.async_gather_candidates(args, context) abort "{{{ let continuation = a:context.source__continuation let ignore_dir = get(a:context, 'custom_rec_ignore_directory_pattern', \ '/\.\+$\|/\%(\.hg\|\.git\|\.bzr\|\.svn\)/') let [continuation.rest, files] = \ s:get_files(a:context, continuation.rest, \ 1, g:unite_source_rec_unit, ignore_dir) if empty(continuation.rest) || ( \ g:unite_source_rec_max_cache_files > 0 && \ len(continuation.files) > \ g:unite_source_rec_max_cache_files) if !empty(continuation.rest) call unite#print_source_message( \ 'Too many candidates.', self.name) endif " Disable async. let a:context.is_async = 0 let continuation.end = 1 endif let candidates = unite#helper#ignore_candidates( \ unite#helper#paths2candidates(files), a:context) let continuation.files += candidates if empty(continuation.rest) call s:write_cache(a:context, \ a:context.source__directory, continuation.files) endif return deepcopy(candidates) endfunction"}}} function! s:source_file_rec.hooks.on_init(args, context) abort "{{{ let a:context.source__is_directory = 0 call s:on_init(a:args, a:context, s:source_file_rec.name) endfunction"}}} function! s:source_file_rec.vimfiler_check_filetype(args, context) abort "{{{ let path = unite#util#substitute_path_separator( \ unite#util#expand(join(a:args, ':'))) let path = unite#util#substitute_path_separator( \ simplify(fnamemodify(path, ':p'))) if isdirectory(path) let type = 'directory' let lines = [] let dict = {} else return [] endif return [type, lines, dict] endfunction"}}} function! s:source_file_rec.vimfiler_gather_candidates(args, context) abort "{{{ let path = get(s:get_paths(a:args, a:context), 0, '') if !isdirectory(path) let a:context.source__directory = path return [] endif " Initialize. let candidates = copy(self.gather_candidates(a:args, a:context)) while a:context.is_async " Gather all candidates. " User input check. echo 'File searching...(if press any key, will cancel.)' redraw if getchar(0) break endif let candidates += self.async_gather_candidates(a:args, a:context) endwhile redraw! let old_dir = getcwd() if path !=# old_dir call unite#util#lcd(path) endif let exts = unite#util#is_windows() ? \ escape(substitute($PATHEXT . ';.LNK', ';', '\\|', 'g'), '.') : '' " Set vimfiler property. for candidate in candidates call unite#sources#file#create_vimfiler_dict(candidate, exts) endfor if path !=# old_dir call unite#util#lcd(old_dir) endif return deepcopy(candidates) endfunction"}}} function! s:source_file_rec.vimfiler_dummy_candidates(args, context) abort "{{{ let path = unite#util#substitute_path_separator( \ unite#util#expand(join(a:args, ':'))) let path = unite#util#substitute_path_separator( \ simplify(fnamemodify(path, ':p'))) if path == '' return [] endif let old_dir = getcwd() if path !=# old_dir call unite#util#lcd(path) endif let exts = unite#util#is_windows() ? \ escape(substitute($PATHEXT . ';.LNK', ';', '\\|', 'g'), '.') : '' " Set vimfiler property. let candidates = [ unite#sources#file#create_file_dict(path, '') ] for candidate in candidates call unite#sources#file#create_vimfiler_dict(candidate, exts) endfor if path !=# old_dir call unite#util#lcd(old_dir) endif return deepcopy(candidates) endfunction"}}} function! s:source_file_rec.vimfiler_complete(args, context, arglead, cmdline, cursorpos) abort "{{{ return unite#sources#file#complete_directory( \ a:args, a:context, a:arglead, a:cmdline, a:cursorpos) endfunction"}}} function! s:source_file_rec.complete(args, context, arglead, cmdline, cursorpos) abort "{{{ return unite#sources#file#complete_directory( \ a:args, a:context, a:arglead, a:cmdline, a:cursorpos) endfunction"}}} " Source async. let s:source_file_async = deepcopy(s:source_file_rec) let s:source_file_async.name = 'file_rec/async' let s:source_file_async.description = \ 'asynchronous candidates from directory by recursive' function! s:source_file_async.gather_candidates(args, context) abort "{{{ let paths = s:get_paths(a:args, a:context) let a:context.source__directory = join(paths, "\n") if !unite#util#has_vimproc() call unite#print_source_message( \ 'vimproc plugin is not installed.', self.name) let a:context.is_async = 0 return [] endif let directory = a:context.source__directory call unite#print_source_message( \ 'directory: ' . directory, self.name) call s:init_continuation(a:context, directory) let continuation = a:context.source__continuation if empty(continuation.rest) || continuation.end " Disable async. let a:context.is_async = 0 let continuation.end = 1 return deepcopy(continuation.files) endif if type(g:unite_source_rec_async_command) == type('') " You must specify list type. call unite#print_source_message( \ 'g:unite_source_rec_async_command must be list type.', self.name) let a:context.is_async = 0 return [] endif let args = g:unite_source_rec_async_command if a:context.source__is_directory " Use find command. let args = ['find', '-L'] endif if empty(args) || !executable(args[0]) if empty(args) call unite#print_source_message( \ 'You must install file list command and specify ' \ . 'g:unite_source_rec_async_command variable.', self.name) else call unite#print_source_message('async command : "'. \ args[0].'" is not executable.', self.name) endif let a:context.is_async = 0 return [] endif " Note: If find command and args used, uses whole command line. let commands = args + paths if args[0] ==# 'find' " Default option. let commands += g:unite_source_rec_find_args let commands += \ ['-o', '-type', \ (a:context.source__is_directory ? 'd' : 'f'), '-print'] endif call unite#add_source_message( \ 'Command-line: ' . string(commands), self.name) let a:context.source__proc = vimproc#popen3(commands, \ unite#helper#is_pty(args[0])) " Close handles. call a:context.source__proc.stdin.close() return [] endfunction"}}} function! s:source_file_async.async_gather_candidates(args, context) abort "{{{ let stderr = a:context.source__proc.stderr if !stderr.eof " Print error. let errors = filter(unite#util#read_lines(stderr, 200), \ "v:val !~ '^\\s*$'") if !empty(errors) call unite#print_source_error(errors, self.name) endif endif let continuation = a:context.source__continuation let stdout = a:context.source__proc.stdout let paths = map(filter( \ unite#util#read_lines(stdout, 2000), 'v:val != ""'), \ "unite#util#iconv(v:val, 'char', &encoding)") if unite#util#is_windows() let paths = map(paths, 'unite#util#substitute_path_separator(v:val)') endif let candidates = unite#helper#ignore_candidates( \ unite#helper#paths2candidates(paths), a:context) if stdout.eof || ( \ g:unite_source_rec_max_cache_files > 0 && \ len(continuation.files) > \ g:unite_source_rec_max_cache_files) " Disable async. if !stdout.eof call unite#print_source_message( \ 'Too many candidates.', self.name) endif let a:context.is_async = 0 let continuation.end = 1 call a:context.source__proc.waitpid() endif let continuation.files += candidates if stdout.eof call s:write_cache(a:context, \ a:context.source__directory, continuation.files) endif return deepcopy(candidates) endfunction"}}} function! s:source_file_async.hooks.on_init(args, context) abort "{{{ let a:context.source__is_directory = 0 call s:on_init(a:args, a:context, s:source_file_async.name) endfunction"}}} function! s:source_file_async.hooks.on_close(args, context) abort "{{{ if has_key(a:context, 'source__proc') call a:context.source__proc.kill() endif endfunction "}}} " Source neovim. let s:source_file_neovim = deepcopy(s:source_file_rec) let s:source_file_neovim.name = 'file_rec/neovim' let s:source_file_neovim.description = \ 'neovim asynchronous candidates from directory by recursive' let s:job_info = {} function! s:job_handler(job_id, data, event) abort "{{{ if !has_key(s:job_info, a:job_id) let s:job_info[a:job_id] = { \ 'candidates' : [], \ 'errors' : [], \ 'eof' : 0, \ } endif let job = s:job_info[a:job_id] if a:event ==# 'exit' let job.eof = 1 return endif let lines = a:data let candidates = (a:event ==# 'stdout') ? job.candidates : job.errors if !empty(lines) && !empty(candidates) \ && !filereadable(candidates[-1]) && candidates[-1] !~ '\r$' " Join to the previous line let candidates[-1] .= lines[0] call remove(lines, 0) endif call map(filter(lines, 'v:val != ""'), \ "substitute(unite#util#iconv( \ v:val, 'char', &encoding), '\\r$', '', '')") if unite#util#is_windows() call map(lines, \ 'unite#util#substitute_path_separator(v:val)') endif let candidates += lines endfunction"}}} function! s:source_file_neovim.gather_candidates(args, context) abort "{{{ let paths = s:get_paths(a:args, a:context) let a:context.source__directory = join(paths, "\n") if !has('nvim') call unite#print_source_message( \ 'Your vim is not neovim.', self.name) let a:context.is_async = 0 return [] endif let directory = a:context.source__directory call unite#print_source_message( \ 'directory: ' . directory, self.name) call s:init_continuation(a:context, directory) let continuation = a:context.source__continuation if empty(continuation.rest) || continuation.end " Disable async. let a:context.is_async = 0 let continuation.end = 1 return deepcopy(continuation.files) endif if type(g:unite_source_rec_async_command) == type('') " You must specify list type. call unite#print_source_message( \ 'g:unite_source_rec_async_command must be list type.', self.name) let a:context.is_async = 0 return [] endif let args = g:unite_source_rec_async_command if a:context.source__is_directory " Use find command. let args = ['find', '-L'] endif if empty(args) || !executable(args[0]) if empty(args) call unite#print_source_message( \ 'You must install file list command and specify ' \ . 'g:unite_source_rec_async_command variable.', self.name) else call unite#print_source_message('async command : "'. \ args[0].'" is not executable.', self.name) endif let a:context.is_async = 0 return [] endif " Note: If find command and args used, uses whole command line. let commands = args + paths if args[0] ==# 'find' " Default option. let commands += g:unite_source_rec_find_args let commands += \ ['-o', '-type', \ (a:context.source__is_directory ? 'd' : 'f'), '-print'] endif call unite#add_source_message( \ 'Command-line: ' . string(commands), self.name) let a:context.source__job = jobstart(commands, { \ 'on_stdout' : function('s:job_handler'), \ 'on_stderr' : function('s:job_handler'), \ 'on_exit' : function('s:job_handler'), \ 'pty' : unite#helper#is_pty(args[0]), \ }) return [] endfunction"}}} function! s:source_file_neovim.async_gather_candidates(args, context) abort "{{{ if !has_key(s:job_info, a:context.source__job) return [] endif let job = s:job_info[a:context.source__job] if !empty(job.errors) " Print error. call unite#print_source_error(job.errors[: -2], self.name) let job.errors = job.errors[-1:] endif let continuation = a:context.source__continuation let candidates = job.eof ? job.candidates : job.candidates[: -2] let candidates = unite#helper#ignore_candidates( \ unite#helper#paths2candidates(candidates), a:context) let job.candidates = job.eof ? [] : job.candidates[-1:] if job.eof " Disable async. let a:context.is_async = 0 let continuation.end = 1 call s:source_file_neovim.hooks.on_close(a:args, a:context) endif let continuation.files += candidates if job.eof call s:write_cache(a:context, \ a:context.source__directory, continuation.files) endif return deepcopy(candidates) endfunction"}}} function! s:source_file_neovim.hooks.on_init(args, context) abort "{{{ let a:context.source__is_directory = 0 call s:on_init(a:args, a:context, s:source_file_neovim.name) endfunction"}}} function! s:source_file_neovim.hooks.on_close(args, context) abort "{{{ if has_key(a:context, 'source__job') \ && has_key(s:job_info, a:context.source__job) silent! call jobstop(a:context.source__job) call remove(s:job_info, a:context.source__job) endif endfunction "}}} " Source git. let s:source_file_git = deepcopy(s:source_file_async) let s:source_file_git.name = 'file_rec/git' let s:source_file_git.description = \ 'git candidates from directory by recursive' function! s:source_file_git.gather_candidates(args, context) abort "{{{ if !unite#util#has_vimproc() call unite#print_source_message( \ 'vimproc plugin is not installed.', self.name) let a:context.is_async = 0 return [] endif let directory = fnamemodify(finddir('.git', ';'), ':p:h:h') if directory == '' let directory = fnamemodify(findfile('.git', ';'), ':p:h') endif let directory = unite#util#substitute_path_separator(directory) if directory == '' " Not in git directory. call unite#print_source_message( \ 'Not in git directory.', self.name) let a:context.is_async = 0 return [] endif let a:context.source__directory = \ unite#util#substitute_path_separator(getcwd()) . '/' call unite#print_source_message( \ 'directory: ' . directory, self.name) call s:init_continuation(a:context, directory) let continuation = a:context.source__continuation if empty(continuation.rest) || continuation.end " Disable async. let a:context.is_async = 0 let continuation.end = 1 return deepcopy(continuation.files) endif let command = g:unite_source_rec_git_command \ . ' ls-files ' . join(a:args) let args = vimproc#parser#split_args(command) + a:args if empty(args) || !executable(args[0]) call unite#print_source_message('git command : "'. \ args[0].'" is not executable.', self.name) let a:context.is_async = 0 return [] endif call unite#add_source_message( \ 'Command-line: ' . command, self.name) let a:context.source__proc = vimproc#popen3(command) " Close handles. call a:context.source__proc.stdin.close() return [] endfunction"}}} function! s:source_file_git.async_gather_candidates(args, context) abort "{{{ return map(s:source_file_async.async_gather_candidates( \ a:args, a:context), "{ \ 'word' : a:context.source__directory . v:val.word, \ 'action__path' : a:context.source__directory . v:val.word, \}") endfunction"}}} function! s:source_file_git.complete(args, context, arglead, cmdline, cursorpos) abort "{{{ return [] endfunction"}}} " Source directory. let s:source_directory_rec = deepcopy(s:source_file_rec) let s:source_directory_rec.name = 'directory_rec' let s:source_directory_rec.description = \ 'candidates from directory by recursive' let s:source_directory_rec.default_kind = 'directory' function! s:source_directory_rec.hooks.on_init(args, context) abort "{{{ let a:context.source__is_directory = 1 call s:on_init(a:args, a:context, s:source_directory_rec.name) endfunction"}}} function! s:source_directory_rec.hooks.on_post_filter(args, context) abort "{{{ for candidate in filter(copy(a:context.candidates), \ "v:val.word[-1:] != '/'") let candidate.abbr = candidate.word . '/' endfor endfunction"}}} " Source directory/async. let s:source_directory_async = deepcopy(s:source_file_async) let s:source_directory_async.name = 'directory_rec/async' let s:source_directory_async.description = \ 'asynchronous candidates from directory by recursive' let s:source_directory_async.default_kind = 'directory' function! s:source_directory_async.hooks.on_init(args, context) abort "{{{ let a:context.source__is_directory = 1 call s:on_init(a:args, a:context, s:source_directory_async.name) endfunction"}}} function! s:source_directory_async.hooks.on_post_filter(args, context) abort "{{{ for candidate in filter(copy(a:context.candidates), \ "v:val.word[-1:] != '/'") let candidate.abbr = candidate.word . '/' endfor endfunction"}}} " Misc. function! s:get_paths(args, context) abort "{{{ let args = unite#helper#parse_source_args(a:args) let directory = get(args, 0, '') if directory == '' let directory = isdirectory(a:context.path) ? \ a:context.path : getcwd() endif let paths = [] for path in split(directory, "\n") let path = unite#util#substitute_path_separator( \ fnamemodify(unite#util#expand(path), ':p')) if path != '/' && path =~ '/$' let path = path[: -2] endif call add(paths, path) endfor return paths endfunction"}}} function! s:get_files(context, files, level, max_unit, ignore_dir) abort "{{{ let continuation_files = [] let ret_files = [] let files_index = 0 let ret_files_len = 0 for file in a:files let files_index += 1 if isdirectory(file) if file =~? a:ignore_dir continue endif if getftype(file) ==# 'link' let real_file = s:resolve(file) if real_file == '' continue endif endif if file != '/' && file =~ '/$' let file = file[: -2] endif if a:context.source__is_directory && \ file !=# a:context.source__directory call add(ret_files, file) let ret_files_len += 1 endif let child_index = 0 let children = exists('*vimproc#readdir') ? \ vimproc#readdir(file) : \ unite#util#glob(file.'/*') for child in children let child = substitute(child, '\/$', '', '') let child_index += 1 if child =~? a:ignore_dir continue endif if isdirectory(child) if getftype(child) ==# 'link' let real_file = s:resolve(child) if real_file == '' continue endif endif if a:context.source__is_directory call add(ret_files, child) let ret_files_len += 1 endif if a:level < 5 && ret_files_len < a:max_unit let [continuation_files_child, ret_files_child] = \ s:get_files(a:context, [child], a:level + 1, \ a:max_unit - ret_files_len, a:ignore_dir) let continuation_files += continuation_files_child if !a:context.source__is_directory let ret_files += ret_files_child let ret_files_len += len(ret_files_child) endif else call add(continuation_files, child) endif elseif !a:context.source__is_directory call add(ret_files, child) let ret_files_len += 1 if ret_files_len > a:max_unit let continuation_files += children[child_index :] break endif endif endfor elseif !a:context.source__is_directory call add(ret_files, file) let ret_files_len += 1 endif if ret_files_len > a:max_unit break endif endfor let continuation_files += a:files[files_index :] return [continuation_files, map(ret_files, \ "unite#util#substitute_path_separator(fnamemodify(v:val, ':p'))")] endfunction"}}} function! s:on_init(args, context, name) abort "{{{ augroup plugin-unite-source-file_rec autocmd! autocmd BufEnter,BufWinEnter,BufFilePost,BufWritePost * \ call unite#sources#rec#_append() augroup END let a:context.source__name = a:name endfunction"}}} function! s:init_continuation(context, directory) abort "{{{ let cache_dir = printf('%s/%s/%s', \ unite#get_data_directory(), \ a:context.source__name, \ (a:context.source__is_directory ? 'directory' : 'file')) let continuation = (a:context.source__is_directory) ? \ s:continuation.directory : s:continuation.file if a:context.is_redraw " Delete old cache files. call s:Cache.deletefile(cache_dir, a:directory) endif if s:Cache.filereadable(cache_dir, a:directory) " Use cache file. let files = unite#helper#paths2candidates( \ s:Cache.readfile(cache_dir, a:directory)) let continuation[a:directory] = { \ 'files' : files, \ 'rest' : [], \ 'directory' : a:directory, 'end' : 1, \ } else let a:context.is_async = 1 let continuation[a:directory] = { \ 'files' : [], 'rest' : [a:directory], \ 'directory' : a:directory, 'end' : 0, \ } endif let a:context.source__continuation = continuation[a:directory] let a:context.source__continuation.files = \ filter(copy(a:context.source__continuation.files), \ (a:context.source__is_directory) ? \ 'isdirectory(v:val.action__path)' : \ 'filereadable(v:val.action__path)') endfunction"}}} function! s:write_cache(context, directory, files) abort "{{{ let cache_dir = printf('%s/%s/%s', \ unite#get_data_directory(), \ a:context.source__name, \ (a:context.source__is_directory ? 'directory' : 'file')) if g:unite_source_rec_min_cache_files >= 0 \ && !unite#util#is_sudo() \ && len(a:files) > \ g:unite_source_rec_min_cache_files call s:Cache.writefile(cache_dir, a:directory, \ map(copy(a:files), 'v:val.action__path')) elseif s:Cache.filereadable(cache_dir, a:directory) " Delete old cache files. call s:Cache.deletefile(cache_dir, a:directory) endif endfunction"}}} function! unite#sources#rec#_append() abort "{{{ let path = expand('%:p') if path !~ '\a\+:' let path = simplify(resolve(path)) endif " Append the current buffer to the mru list. if !filereadable(path) || &l:buftype =~# 'help\|nofile' return endif let path = unite#util#substitute_path_separator(path) " Check continuation. let base_path = unite#util#substitute_path_separator( \ fnamemodify(path, ':h')) . '/' for continuation in values(filter(copy(s:continuation.file), \ "stridx(v:key.'/', base_path) == 0")) let continuation.files = unite#util#uniq(add( \ continuation.files, { \ 'word' : path, 'action__path' : path, \ })) endfor endfunction"}}} function! unite#sources#rec#define() abort "{{{ let sources = [ s:source_file_rec, s:source_directory_rec ] let sources += [ s:source_file_async, s:source_directory_async] let sources += [ s:source_file_git ] let sources += [ s:source_file_neovim ] return sources endfunction"}}} function! s:resolve(file) abort "{{{ " Detect symbolic link loop. let file_link = unite#util#substitute_path_separator( \ resolve(a:file)) return stridx(a:file, file_link.'/') == 0 ? '' : file_link endfunction"}}} let &cpo = s:save_cpo unlet s:save_cpo " vim: foldmethod=marker