1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-01-24 06:20:05 +08:00
SpaceVim/bundle/vim-grepper/plugin/grepper.vim
2020-06-13 14:06:35 +08:00

1252 lines
38 KiB
VimL

" Initialization {{{1
if exists('g:loaded_grepper')
finish
endif
let g:loaded_grepper = 1
" Escaping test line:
" ..ad\\f40+$':-# @=,!;%^&&*()_{}/ /4304\'""?`9$343%$ ^adfadf[ad)[(
highlight default link GrepperPrompt Question
"
" Default values that get used for missing values in g:grepper.
"
let s:defaults = {
\ 'quickfix': 1,
\ 'open': 1,
\ 'switch': 1,
\ 'jump': 0,
\ 'cword': 0,
\ 'prompt': 1,
\ 'prompt_text': '$c> ',
\ 'prompt_quote': 0,
\ 'highlight': 0,
\ 'buffer': 0,
\ 'buffers': 0,
\ 'append': 0,
\ 'searchreg': 0,
\ 'side': 0,
\ 'side_cmd': 'vnew',
\ 'stop': 5000,
\ 'dir': 'cwd',
\ 'prompt_mapping_tool': '<tab>',
\ 'prompt_mapping_dir': '<c-d>',
\ 'prompt_mapping_side': '<c-s>',
\ 'repo': ['.git', '.hg', '.svn'],
\ 'tools': ['git', 'ag', 'ack', 'ack-grep', 'grep', 'findstr', 'rg', 'pt', 'sift'],
\ 'git': { 'grepprg': 'git grep -nGI',
\ 'grepformat': '%f:%l:%c:%m,%f:%l:%m,%f',
\ 'escape': '\^$.*[]' },
\ 'ag': { 'grepprg': 'ag --vimgrep',
\ 'grepformat': '%f:%l:%c:%m,%f:%l:%m,%f',
\ 'escape': '\^$.*+?()[]{}|' },
\ 'rg': { 'grepprg': 'rg -H --no-heading --vimgrep' . (has('win32') ? ' $* .' : ''),
\ 'grepformat': '%f:%l:%c:%m,%f',
\ 'escape': '\^$.*+?()[]{}|' },
\ 'pt': { 'grepprg': 'pt --nogroup',
\ 'grepformat': '%f:%l:%m,%f' },
\ 'sift': { 'grepprg': 'sift -n --column --binary-skip $* .',
\ 'grepprgbuf': 'sift -n --column --binary-skip --filename -- $* $.',
\ 'grepformat': '%f:%l:%c:%m,%f',
\ 'escape': '\+*?^$%#()[]' },
\ 'ack': { 'grepprg': 'ack --noheading --column',
\ 'grepformat': '%f:%l:%c:%m,%f',
\ 'escape': '\^$.*+?()[]{}|' },
\ 'ack-grep': { 'grepprg': 'ack-grep --noheading --column',
\ 'grepformat': '%f:%l:%c:%m,%f',
\ 'escape': '\^$.*+?()[]{}|' },
\ 'grep': { 'grepprg': 'grep -RIn $* .',
\ 'grepprgbuf': 'grep -HIn -- $* $.',
\ 'grepformat': '%f:%l:%m,%f',
\ 'escape': '\^$.*[]' },
\ 'findstr': { 'grepprg': 'findstr -rspnc:$* *',
\ 'grepprgbuf': 'findstr -rpnc:$* $.',
\ 'grepformat': '%f:%l:%m,%f',
\ 'wordanchors': ['\<', '\>'] }
\ }
" Make it possible to configure the global and operator behaviours separately.
let s:defaults.operator = deepcopy(s:defaults)
let s:defaults.operator.prompt = 0
let s:has_doau_modeline = v:version > 703 || v:version == 703 && has('patch442')
function! s:merge_configs(config, defaults) abort
let new = deepcopy(a:config)
" Add all missing default options.
call extend(new, a:defaults, 'keep')
" Global options.
for k in keys(a:config)
if k == 'operator'
continue
endif
" If only part of an option dict was set, add the missing default keys.
if type(new[k]) == type({}) && has_key(a:defaults, k) && new[k] != a:defaults[k]
call extend(new[k], a:defaults[k], 'keep')
endif
" Inherit operator option from global option unless it already exists or
" has a default value where the global option has not.
if !has_key(new.operator, k) || (has_key(a:defaults, k)
\ && new[k] != a:defaults[k]
\ && new.operator[k] == s:defaults.operator[k])
let new.operator[k] = deepcopy(new[k])
endif
endfor
" Operator options.
if has_key(a:config, 'operator')
for opt in keys(a:config.operator)
" If only part of an operator option dict was set, inherit the missing
" keys from the global option.
if type(new.operator[opt]) == type({}) && new.operator[opt] != new[opt]
call extend(new.operator[opt], new[opt], 'keep')
endif
endfor
endif
return new
endfunction
let g:grepper = exists('g:grepper')
\ ? s:merge_configs(g:grepper, s:defaults)
\ : deepcopy(s:defaults)
for s:tool in g:grepper.tools
if !has_key(g:grepper, s:tool)
\ || !has_key(g:grepper[s:tool], 'grepprg')
\ || !executable(expand(matchstr(g:grepper[s:tool].grepprg, '^[^ ]*')))
call remove(g:grepper.tools, index(g:grepper.tools, s:tool))
endif
endfor
"
" Special case: ack (different distros use different names for ack)
" Prefer ack-grep since its presence likely means ack is a different tool.
"
let s:ack = index(g:grepper.tools, 'ack')
let s:ackgrep = index(g:grepper.tools, 'ack-grep')
if (s:ack >= 0) && (s:ackgrep >= 0)
call remove(g:grepper.tools, s:ack)
endif
let s:cmdline = ''
let s:slash = exists('+shellslash') && !&shellslash ? '\' : '/'
let s:git_column_flag_checked = 0
" Job handlers {{{1
" s:on_stdout_nvim() {{{2
function! s:on_stdout_nvim(_job_id, data, _event) dict abort
if !exists('s:id')
return
endif
let orig_dir = s:chdir_push(self.work_dir)
let lcandidates = []
try
if len(a:data) > 1 || empty(a:data[-1])
" Second-last item is the last complete line in a:data.
let acc_line = self.stdoutbuf . a:data[0]
let lcandidates = (empty(acc_line) ? [] : [acc_line]) + a:data[1:-2]
let self.stdoutbuf = ''
endif
" Last item in a:data is an incomplete line (or empty), append to buffer
let self.stdoutbuf .= a:data[-1]
if self.flags.stop > 0 && (self.num_matches + len(lcandidates) >= self.flags.stop)
" Add the remaining data
let n_rem_lines = self.flags.stop - self.num_matches
if n_rem_lines > 0
noautocmd execute self.addexpr 'lcandidates[:n_rem_lines-1]'
let self.num_matches = self.flags.stop
endif
silent! call jobstop(s:id)
unlet! s:id
return
else
noautocmd execute self.addexpr 'lcandidates'
let self.num_matches += len(lcandidates)
endif
finally
call s:chdir_pop(orig_dir)
endtry
endfunction
" s:on_stdout_vim() {{{2
function! s:on_stdout_vim(_job_id, data) dict abort
if !exists('s:id')
return
endif
let orig_dir = s:chdir_push(self.work_dir)
try
noautocmd execute self.addexpr 'a:data'
let self.num_matches += 1
if self.flags.stop > 0 && self.num_matches >= self.flags.stop
silent! call job_stop(s:id)
unlet! s:id
endif
finally
call s:chdir_pop(orig_dir)
endtry
endfunction
" s:on_exit() {{{2
function! s:on_exit(...) dict abort
execute 'tabnext' self.tabpage
execute self.window .'wincmd w'
unlet! s:id
return s:finish_up(self.flags)
endfunction
" Completion {{{1
" grepper#complete() {{{2
function! grepper#complete(lead, line, _pos) abort
if a:lead =~ '^-'
let flags = ['-append', '-buffer', '-buffers', '-cd', '-cword', '-dir',
\ '-grepprg', '-highlight', '-jump', '-open', '-prompt', '-query',
\ '-quickfix', '-side', '-stop', '-switch', '-tool', '-noappend',
\ '-nohighlight', '-nojump', '-noopen', '-noprompt', '-noquickfix',
\ '-noside', '-noswitch']
return filter(map(flags, 'v:val." "'), 'v:val[:strlen(a:lead)-1] ==# a:lead')
elseif a:line =~# '-dir \w*$'
return filter(map(['cwd', 'file', 'filecwd', 'repo'], 'v:val." "'),
\ 'empty(a:lead) || v:val[:strlen(a:lead)-1] ==# a:lead')
elseif a:line =~# '-stop $'
return ['5000']
elseif a:line =~# '-tool \w*$'
return filter(map(sort(copy(g:grepper.tools)), 'v:val." "'),
\ 'empty(a:lead) || v:val[:strlen(a:lead)-1] ==# a:lead')
else
return grepper#complete_files(a:lead, 0, 0)
endif
endfunction
" grepper#complete_files() {{{2
function! grepper#complete_files(lead, _line, _pos)
let [head, path] = s:extract_path(a:lead)
" handle relative paths
if empty(path) || (path =~ '\s$')
return map(split(globpath('.'.s:slash, path.'*'), '\n'), 'head . "." . v:val[1:] . (isdirectory(v:val) ? s:slash : "")')
" handle sub paths
elseif path =~ '^.\/'
return map(split(globpath('.'.s:slash, path[2:].'*'), '\n'), 'head . "." . v:val[1:] . (isdirectory(v:val) ? s:slash : "")')
" handle absolute paths
elseif path[0] == '/'
return map(split(globpath(s:slash, path.'*'), '\n'), 'head . v:val[1:] . (isdirectory(v:val) ? s:slash : "")')
endif
endfunction
" s:extract_path() {{{2
function! s:extract_path(string) abort
let item = split(a:string, '.*\s\zs', 1)
let len = len(item)
if len == 0 | let [head, path] = ['', '']
elseif len == 1 | let [head, path] = ['', item[0]]
elseif len == 2 | let [head, path] = item
else | throw 'The unexpected happened!'
endif
return [head, path]
endfunction
" Statusline {{{1
" #statusline() {{{2
function! grepper#statusline() abort
return s:cmdline
endfunction
" Helpers {{{1
" s:error() {{{2
function! s:error(msg)
redraw
echohl ErrorMsg
echomsg a:msg
echohl NONE
endfunction
" s:lstrip() {{{2
function! s:lstrip(string) abort
return substitute(a:string, '^\s\+', '', '')
endfunction
" s:split_one() {{{2
function! s:split_one(string) abort
let stripped = s:lstrip(a:string)
let first_word = substitute(stripped, '\v^(\S+).*', '\1', '')
let rest = substitute(stripped, '\v^\S+\s*(.*)', '\1', '')
return [first_word, rest]
endfunction
" s:next_tool() {{{2
function! s:next_tool(flags)
let a:flags.tools = a:flags.tools[1:-1] + [a:flags.tools[0]]
endfunction
" s:get_current_tool() {{{2
function! s:get_current_tool(flags) abort
return a:flags[a:flags.tools[0]]
endfunction
" s:get_current_tool_name() {{{2
function! s:get_current_tool_name(flags) abort
return a:flags.tools[0]
endfunction
" s:get_grepprg() {{{2
function! s:get_grepprg(flags) abort
let tool = s:get_current_tool(a:flags)
if a:flags.buffers
return has_key(tool, 'grepprgbuf')
\ ? substitute(tool.grepprgbuf, '\V$.', '$+', '')
\ : tool.grepprg .' -- $* $+'
elseif a:flags.buffer
return has_key(tool, 'grepprgbuf')
\ ? tool.grepprgbuf
\ : tool.grepprg .' -- $* $.'
endif
return tool.grepprg
endfunction
" s:store_errorformat() {{{2
function! s:store_errorformat(flags) abort
let prog = s:get_current_tool(a:flags)
let s:errorformat = &errorformat
let &errorformat = has_key(prog, 'grepformat') ? prog.grepformat : &errorformat
endfunction
" s:restore_errorformat() {{{2
function! s:restore_errorformat() abort
let &errorformat = s:errorformat
endfunction
" s:restore_mapping() {{{2
function! s:restore_mapping(mapping)
if !empty(a:mapping)
execute printf('%s %s%s%s%s %s %s',
\ (a:mapping.noremap ? 'cnoremap' : 'cmap'),
\ (a:mapping.silent ? '<silent>' : '' ),
\ (a:mapping.buffer ? '<buffer>' : '' ),
\ (a:mapping.nowait ? '<nowait>' : '' ),
\ (a:mapping.expr ? '<expr>' : '' ),
\ a:mapping.lhs,
\ substitute(a:mapping.rhs, '\c<sid>', '<SNR>'.a:mapping.sid.'_', 'g'))
endif
endfunction
" s:escape_query() {{{2
function! s:escape_query(flags, query)
let tool = s:get_current_tool(a:flags)
let a:flags.query_escaped = 1
return shellescape(has_key(tool, 'escape')
\ ? escape(a:query, tool.escape)
\ : a:query)
endfunction
" s:unescape_query() {{{2
function! s:unescape_query(flags, query)
let tool = s:get_current_tool(a:flags)
let q = a:query
if has_key(tool, 'escape')
for c in reverse(split(tool.escape, '\zs'))
let q = substitute(q, '\V\\'.c, c, 'g')
endfor
endif
return q
endfunction
" s:requote_query() {{{2
function! s:requote_query(flags) abort
if a:flags.cword
let a:flags.query = s:escape_cword(a:flags, a:flags.query_orig)
else
let is_findstr = s:get_current_tool_name(a:flags) == 'findstr'
if has_key(a:flags, 'query_orig')
let a:flags.query = (is_findstr ? '' : '-- '). s:escape_query(a:flags, a:flags.query_orig)
else
if a:flags.prompt_quote >= 2
let a:flags.query = a:flags.query[1:-2]
else
let a:flags.query = a:flags.query[:-1]
endif
endif
endif
endfunction
" s:escape_cword() {{{2
function! s:escape_cword(flags, cword)
let tool = s:get_current_tool(a:flags)
let escaped_cword = has_key(tool, 'escape')
\ ? escape(a:cword, tool.escape)
\ : a:cword
let wordanchors = has_key(tool, 'wordanchors')
\ ? tool.wordanchors
\ : ['\b', '\b']
if a:cword =~# '^\k'
let escaped_cword = wordanchors[0] . escaped_cword
endif
if a:cword =~# '\k$'
let escaped_cword = escaped_cword . wordanchors[1]
endif
let a:flags.query_orig = a:cword
let a:flags.query_escaped = 1
return shellescape(escaped_cword)
endfunction
" s:compute_working_directory() {{{2
function! s:compute_working_directory(flags) abort
if has_key(a:flags, 'cd')
return a:flags.cd
endif
for dir in split(a:flags.dir, ',')
if dir == 'repo'
if s:get_current_tool_name(a:flags) == 'git'
let dir = systemlist(printf('git -C %s rev-parse --show-toplevel',
\ shellescape(expand('%:p:h'))))
if !v:shell_error
return dir[0]
endif
endif
for repo in g:grepper.repo
let repopath = finddir(repo, expand('%:p:h').';')
if empty(repopath)
let repopath = findfile(repo, expand('%:p:h').';')
endif
if !empty(repopath)
let repopath = fnamemodify(repopath, ':h')
return fnameescape(repopath)
endif
endfor
elseif dir == 'filecwd'
let cwd = getcwd()
let bufdir = expand('%:p:h')
if stridx(bufdir, cwd) != 0
return fnameescape(bufdir)
endif
elseif dir == 'file'
let bufdir = expand('%:p:h')
return fnameescape(bufdir)
elseif dir == 'cwd'
return getcwd()
else
call s:error("Invalid -dir flag '" . a:flags.dir . "'")
endif
endfor
return ''
endfunction
" s:chdir_push() {{{2
function! s:chdir_push(work_dir)
if !empty(a:work_dir)
let cwd = getcwd()
execute 'lcd' a:work_dir
return cwd
endif
return ''
endfunction
" s:chdir_pop() {{{2
function! s:chdir_pop(buf_dir)
if !empty(a:buf_dir)
execute 'lcd' fnameescape(a:buf_dir)
endif
endfunction
" s:get_config() {{{2
function! s:get_config() abort
let g:grepper = exists('g:grepper')
\ ? s:merge_configs(g:grepper, s:defaults)
\ : deepcopy(s:defaults)
let flags = deepcopy(g:grepper)
if exists('b:grepper')
let flags = s:merge_configs(b:grepper, g:grepper)
endif
return flags
endfunction
" s:set_prompt_text() {{{2
function! s:set_prompt_text(flags) abort
let text = get(a:flags, 'simple_prompt') ? '$t> ' : a:flags.prompt_text
let text = substitute(text, '\V$t', s:get_current_tool_name(a:flags), '')
let text = substitute(text, '\V$c', s:get_grepprg(a:flags), '')
return text
endfunction
" s:set_prompt_op() {{{2
function! s:set_prompt_op(op) abort
let s:prompt_op = a:op
return getcmdline()
endfunction
" s:git_add_column_flag() {{{2
function! s:git_add_column_flag(flags) abort
if !empty(filter(copy(a:flags.tools), 'v:val == "git"'))
\ && a:flags.git.grepprg == 'git grep -nI'
let m = matchlist(system('git --version'), '\v \zs(\d+)\.(\d+)')
if !empty(m) && (m[1] > 2 || (m[1] == 2 && m[2] >= 19))
let a:flags.git.grepprg = 'git grep -nI --column' " for current invocation
let g:grepper.git.grepprg = 'git grep -nI --column' " for subsequent invocations
endif
endif
let s:git_column_flag_checked = 1
endfunction
" s:query2vimregexp() {{{2
function! s:query2vimregexp(flags) abort
if has_key(a:flags, 'query_orig')
let query = a:flags.query_orig
else
" Remove any flags at the beginning, e.g. when using '-uu' with rg, but
" keep plain '-'.
let query = substitute(a:flags.query, '\v^\s+', '', '')
let query = substitute(query, '\v\s+$', '', '')
let pos = 0
while 1
let [mtext, mstart, mend] = matchstrpos(query, '\v^-\S+\s*', pos)
if mstart < 0
break
endif
let pos = mend
if mtext =~ '\v^--\s*$'
break
endif
endwhile
let query = strpart(query, pos)
endif
" Change Vim's '\'' to ' so it can be understood by /.
let vim_query = substitute(query, "'\\\\''", "'", 'g')
" Remove surrounding quotes that denote a string.
let start = vim_query[0]
let end = vim_query[-1:-1]
if start == end && start =~ "\['\"]"
let vim_query = vim_query[1:-2]
endif
if a:flags.query_escaped
let vim_query = s:unescape_query(a:flags, vim_query)
let vim_query = escape(vim_query, '\')
if a:flags.cword
if a:flags.query_orig =~# '^\k'
let vim_query = '\<' . vim_query
endif
if a:flags.query_orig =~# '\k$'
let vim_query = vim_query . '\>'
endif
endif
let vim_query = '\V'. vim_query
else
" \bfoo\b -> \<foo\> Assume only one pair.
let vim_query = substitute(vim_query, '\v\\b(.{-})\\b', '\\<\1\\>', '')
" *? -> \{-}
let vim_query = substitute(vim_query, '*\\\=?', '\\{-}', 'g')
" +? -> \{-1,}
let vim_query = substitute(vim_query, '\\\=+\\\=?', '\\{-1,}', 'g')
let vim_query = escape(vim_query, '+')
endif
return vim_query
endfunction
" }}}1
" s:parse_flags() {{{1
function! s:parse_flags(args) abort
let flags = s:get_config()
let flags.query = ''
let flags.query_escaped = 0
let [flag, args] = s:split_one(a:args)
while !empty(flag)
if flag =~? '\v^-%(no)?(quickfix|qf)$' | let flags.quickfix = flag !~? '^-no'
elseif flag =~? '\v^-%(no)?open$' | let flags.open = flag !~? '^-no'
elseif flag =~? '\v^-%(no)?switch$' | let flags.switch = flag !~? '^-no'
elseif flag =~? '\v^-%(no)?jump$' | let flags.jump = flag !~? '^-no'
elseif flag =~? '\v^-%(no)?prompt$' | let flags.prompt = flag !~? '^-no'
elseif flag =~? '\v^-%(no)?highlight$' | let flags.highlight = flag !~? '^-no'
elseif flag =~? '\v^-%(no)?buffer$' | let flags.buffer = flag !~? '^-no'
elseif flag =~? '\v^-%(no)?buffers$' | let flags.buffers = flag !~? '^-no'
elseif flag =~? '\v^-%(no)?append$' | let flags.append = flag !~? '^-no'
elseif flag =~? '\v^-%(no)?side$' | let flags.side = flag !~? '^-no'
elseif flag =~? '^-cword$' | let flags.cword = 1
elseif flag =~? '^-stop$'
if empty(args) || args[0] =~ '^-'
let flags.stop = -1
else
let [numstring, args] = s:split_one(args)
let flags.stop = str2nr(numstring)
endif
elseif flag =~? '^-dir$'
let [dir, args] = s:split_one(args)
if empty(dir)
call s:error('Missing argument for: -dir')
else
let flags.dir = dir
endif
elseif flag =~? '^-grepprg$'
if empty(args)
call s:error('Missing argument for: -grepprg')
else
if !exists('tool')
let tool = g:grepper.tools[0]
endif
let flags.tools = [tool]
let flags[tool] = copy(g:grepper[tool])
let flags[tool].grepprg = args
endif
break
elseif flag =~? '^-query$'
if empty(args)
" No warning message here. This allows for..
" nnoremap ... :Grepper! -tool ag -query<space>
" ..thus you get nicer file completion.
else
let flags.query = args
endif
break
elseif flag =~? '^-tool$'
let [tool, args] = s:split_one(args)
if tool == ''
call s:error('Missing argument for: -tool')
break
endif
if index(g:grepper.tools, tool) >= 0
let flags.tools =
\ [tool] + filter(copy(g:grepper.tools), 'v:val != tool')
else
call s:error('No such tool: '. tool)
endif
elseif flag ==# '-cd'
if empty(args)
call s:error('Missing argument for: -cd')
break
endif
let dir = fnamemodify(args, ':p')
if !isdirectory(dir)
call s:error('Invalid directory: '. dir)
break
endif
let flags.cd = dir
break
else
call s:error('Ignore unknown flag: '. flag)
endif
let [flag, args] = s:split_one(args)
endwhile
return s:start(flags)
endfunction
" s:process_flags() {{{1
function! s:process_flags(flags)
if a:flags.stop == -1
if exists('s:id')
if has('nvim')
silent! call jobstop(s:id)
else
silent! call job_stop(s:id)
endif
unlet! s:id
endif
return 1
endif
let s:tmp_work_dir = s:compute_working_directory(a:flags)
if s:get_current_tool_name(a:flags) ==# 'git'
\ && empty(finddir('.git', s:tmp_work_dir.';'))
\ && empty(findfile('.git', s:tmp_work_dir.';'))
call remove(a:flags.tools, 0)
if empty(a:flags.tools)
call s:error('Using git outside of repo and no other tool to switch to. Try ":Grepper -dir repo,file" instead.')
return 1
endif
endif
if a:flags.buffer
let a:flags.buflist = [fnamemodify(bufname(''), ':p')]
if !filereadable(a:flags.buflist[0])
call s:error('This buffer is not backed by a file!')
return 1
endif
endif
if a:flags.buffers
let a:flags.buflist = filter(map(filter(range(1, bufnr('$')),
\ 'bufloaded(v:val)'), 'fnamemodify(bufname(v:val), ":p")'), 'filereadable(v:val)')
if empty(a:flags.buflist)
call s:error('No buffer is backed by a file!')
return 1
endif
endif
if a:flags.cword
let a:flags.query = s:escape_cword(a:flags, expand('<cword>'))
endif
if a:flags.prompt
call s:prompt(a:flags)
if s:prompt_op == 'cancelled'
return 1
endif
if a:flags.query =~ '^\s*$'
let a:flags.query = s:escape_cword(a:flags, expand('<cword>'))
" input() got empty input, so no query was added to the history.
call histadd('input', a:flags.query)
elseif a:flags.prompt_quote == 1
let a:flags.query = shellescape(a:flags.query)
endif
else
" input() was skipped, so add query to the history manually.
call histadd('input', a:flags.query)
endif
if a:flags.side
let a:flags.highlight = 1
let a:flags.open = 0
endif
if a:flags.searchreg || a:flags.highlight
let @/ = s:query2vimregexp(a:flags)
call histadd('search', @/)
if a:flags.highlight
call feedkeys(":set hls\<bar>echo\<cr>", 'n')
endif
endif
return 0
endfunction
" s:start() {{{1
function! s:start(flags) abort
let s:prompt_op = ''
if empty(g:grepper.tools)
call s:error('No grep tool found!')
return
endif
if !s:git_column_flag_checked
call s:git_add_column_flag(a:flags)
endif
if s:process_flags(a:flags)
return
endif
return s:run(a:flags)
endfunction
" s:prompt() {{{1
function! s:prompt(flags)
let prompt_text = s:set_prompt_text(a:flags)
if s:prompt_op == 'flag_dir'
let changed_mode = '[-dir '. a:flags.dir .'] '
let prompt_text = changed_mode . prompt_text
elseif s:prompt_op == 'flag_side'
let changed_mode = '['. (a:flags.side ? '-side' : '-noside') .'] '
let prompt_text = changed_mode . prompt_text
endif
" Store original mappings
let mapping_cr = maparg('<cr>', 'c', '', 1)
let mapping_tool = maparg(get(g:grepper, 'next_tool', g:grepper.prompt_mapping_tool), 'c', '', 1)
let mapping_dir = maparg(g:grepper.prompt_mapping_dir, 'c', '', 1)
let mapping_side = maparg(g:grepper.prompt_mapping_side, 'c', '', 1)
" Set plugin-specific mappings
cnoremap <silent> <cr> <c-\>e<sid>set_prompt_op('cr')<cr><cr>
execute 'cnoremap <silent>' g:grepper.prompt_mapping_tool "\<c-\>e\<sid>set_prompt_op('flag_tool')<cr><cr>"
execute 'cnoremap <silent>' g:grepper.prompt_mapping_dir "\<c-\>e\<sid>set_prompt_op('flag_dir')<cr><cr>"
execute 'cnoremap <silent>' g:grepper.prompt_mapping_side "\<c-\>e\<sid>set_prompt_op('flag_side')<cr><cr>"
" Set low timeout for key codes, so <esc> would cancel prompt faster
let ttimeoutsave = &ttimeout
let ttimeoutlensave = &ttimeoutlen
let &ttimeout = 1
let &ttimeoutlen = 100
if a:flags.prompt_quote == 2 && !has_key(a:flags, 'query_orig')
let a:flags.query = "'". a:flags.query ."'\<left>"
elseif a:flags.prompt_quote == 3 && !has_key(a:flags, 'query_orig')
let a:flags.query = '"'. a:flags.query ."\"\<left>"
else
let a:flags.query = a:flags.query
endif
" s:prompt_op indicates which key ended the prompt's input() and is needed to
" distinguish different actions.
" 'cancelled': don't start searching
" 'flag_tool': don't start searching; toggle -tool flag
" 'flag_dir': don't start searching; toggle -dir flag
" 'flag_side': don't start searching; toggle -side flag
" 'cr': start searching
let s:prompt_op = 'cancelled'
echohl GrepperPrompt
call inputsave()
try
if has('nvim-0.3.4')
let a:flags.query = input({
\ 'prompt': prompt_text,
\ 'default': a:flags.query,
\ 'completion': 'customlist,grepper#complete_files',
\ 'highlight': { cmdline -> [[0, len(cmdline), 'String']] },
\ })
else
let a:flags.query = input(prompt_text, a:flags.query,
\ 'customlist,grepper#complete_files')
endif
catch /^Vim:Interrupt$/ " Ctrl-c was pressed
let s:prompt_op = 'cancelled'
finally
redraw!
" Restore mappings
cunmap <cr>
execute 'cunmap' g:grepper.prompt_mapping_tool
execute 'cunmap' g:grepper.prompt_mapping_dir
execute 'cunmap' g:grepper.prompt_mapping_side
call s:restore_mapping(mapping_cr)
call s:restore_mapping(mapping_tool)
call s:restore_mapping(mapping_dir)
call s:restore_mapping(mapping_side)
" Restore original timeout settings for key codes
let &ttimeout = ttimeoutsave
let &ttimeoutlen = ttimeoutlensave
echohl NONE
call inputrestore()
endtry
if s:prompt_op != 'cr' && s:prompt_op != 'cancelled'
if s:prompt_op == 'flag_tool'
call s:next_tool(a:flags)
elseif s:prompt_op == 'flag_dir'
let states = ['cwd', 'file', 'filecwd', 'repo']
let pattern = printf('v:val =~# "^%s.*"', a:flags.dir)
let current_index = index(map(copy(states), pattern), 1)
let a:flags.dir = states[(current_index + 1) % len(states)]
let s:tmp_work_dir = s:compute_working_directory(a:flags)
elseif s:prompt_op == 'flag_side'
let a:flags.side = !a:flags.side
endif
call s:requote_query(a:flags)
return s:prompt(a:flags)
endif
endfunction
" s:build_cmdline() {{{1
function! s:build_cmdline(flags) abort
let grepprg = s:get_grepprg(a:flags)
if has_key(a:flags, 'buflist')
if has('win32')
" cmd.exe does not use single quotes for quoting. Using 'noshellslash'
" forces path separators to be backslashes and makes shellescape() using
" double quotes. Beforehand escape all backslashes, otherwise \t in
" 'dir\test' would be considered a tab etc.
let [shellslash, &shellslash] = [&shellslash, 0]
call map(a:flags.buflist, 'shellescape(escape(fnamemodify(v:val, ":."), "\\"))')
let &shellslash = shellslash
else
call map(a:flags.buflist, 'shellescape(fnamemodify(v:val, ":."))')
endif
endif
if stridx(grepprg, '$.') >= 0
let grepprg = substitute(grepprg, '\V$.', a:flags.buflist[0], '')
endif
if stridx(grepprg, '$+') >= 0
let grepprg = substitute(grepprg, '\V$+', join(a:flags.buflist), '')
endif
if stridx(grepprg, '$*') >= 0
let grepprg = substitute(grepprg, '\V$*', escape(a:flags.query, '\&'), 'g')
else
let grepprg .= ' ' . a:flags.query
endif
return grepprg
endfunction
" s:run() {{{1
function! s:run(flags)
if !a:flags.append
if a:flags.quickfix
call setqflist([])
else
call setloclist(0, [])
endif
endif
let orig_dir = s:chdir_push(s:tmp_work_dir)
let s:cmdline = s:build_cmdline(a:flags)
" 'cmd' and 'options' are only used for async execution.
if has('win32')
let cmd = 'cmd.exe /c '. s:cmdline
else
let cmd = ['sh', '-c', s:cmdline]
endif
let options = {
\ 'cmd': s:cmdline,
\ 'work_dir': s:tmp_work_dir,
\ 'flags': a:flags,
\ 'addexpr': a:flags.quickfix ? 'caddexpr' : 'laddexpr',
\ 'window': winnr(),
\ 'tabpage': tabpagenr(),
\ 'stdoutbuf': '',
\ 'num_matches': 0,
\ }
call s:store_errorformat(a:flags)
if &verbose
echomsg 'grepper: running' string(cmd)
endif
let msg = printf('Running: %s', s:cmdline)
if exists('v:echospace') && strwidth(msg) > v:echospace
let msg = printf('%.*S...', v:echospace - 3, msg)
endif
echo msg
if has('nvim')
if exists('s:id')
silent! call jobstop(s:id)
endif
try
let s:id = jobstart(cmd, extend(options, {
\ 'on_stdout': function('s:on_stdout_nvim'),
\ 'on_stderr': function('s:on_stdout_nvim'),
\ 'stdout_buffered': 1,
\ 'stderr_buffered': 1,
\ 'on_exit': function('s:on_exit'),
\ }))
finally
call s:chdir_pop(orig_dir)
endtry
elseif !get(w:, 'testing') && has('patch-7.4.1967')
if exists('s:id')
silent! call job_stop(s:id)
endif
try
let s:id = job_start(cmd, {
\ 'in_io': 'null',
\ 'err_io': 'out',
\ 'out_cb': function('s:on_stdout_vim', options),
\ 'close_cb': function('s:on_exit', options),
\ })
finally
call s:chdir_pop(orig_dir)
endtry
else
try
execute 'silent' (a:flags.quickfix ? 'cgetexpr' : 'lgetexpr') 'system(s:cmdline)'
finally
call s:chdir_pop(orig_dir)
endtry
call s:finish_up(a:flags)
endif
endfunction
" s:finish_up() {{{1
function! s:finish_up(flags)
let qf = a:flags.quickfix
let list = qf ? getqflist() : getloclist(0)
let size = len(list)
let cmdline = s:cmdline
let s:cmdline = ''
call s:restore_errorformat()
try
" TODO: Remove condition if nvim 0.2.0+ enters Debian stable.
let attrs = has('nvim') && !has('nvim-0.2.0')
\ ? cmdline
\ : {'title': cmdline, 'context': {'query': @/}}
if qf
call setqflist(list, a:flags.append ? 'a' : 'r', attrs)
else
call setloclist(0, list, a:flags.append ? 'a' : 'r', attrs)
endif
catch /E118/
endtry
if size == 0
execute (qf ? 'cclose' : 'lclose')
redraw
echo 'No matches found.'
return
endif
if a:flags.jump
execute (qf ? 'cfirst' : 'lfirst')
endif
let has_errors = !empty(filter(list, 'v:val.valid == 0'))
" Also open if the side mode is off and the list contains any invalid entry.
if a:flags.open || (has_errors && !a:flags.side)
execute (qf ? 'botright copen' : 'lopen') (size > 10 ? 10 : size)
let w:quickfix_title = cmdline
setlocal nowrap
if !a:flags.switch
call feedkeys("\<c-w>p", 'n')
endif
endif
redraw
echo printf('Found %d matches.', size)
if a:flags.side
call s:side(a:flags)
endif
if exists('#User#Grepper')
execute 'doautocmd' (s:has_doau_modeline ? '<nomodeline>' : '') 'User Grepper'
endif
endfunction
" }}}1
" -side {{{1
let s:filename_regexp = '\v^%(\>\>\>|\]\]\]) ([[:alnum:][:blank:]\/\-_.~]+):(\d+)'
let s:error_marker = '!^@ERR '
" s:side() {{{2
function! s:side(flags) abort
call s:side_create_window(a:flags)
call s:side_buffer_settings()
endfunction
" s:side_create_window() {{{2
function! s:side_create_window(flags) abort
" Contexts are lists of a fixed format:
"
" [0] = line number of the match
" [1] = start of context
" [2] = end of context
let regions = {}
let errors = []
let list = a:flags.quickfix ? getqflist() : getloclist(0)
" process quickfix entries
for entry in list
let bufname = bufname(entry.bufnr)
if !entry.valid
" collect lines with error messages
call add(errors, entry.text)
continue
endif
if has_key(regions, bufname)
if (regions[bufname][-1][2] + 2) > entry.lnum
" merge entries that are close to each other into the same context
let regions[bufname][-1][2] = entry.lnum + 2
else
" new context in same file
let start = (entry.lnum < 4) ? 0 : (entry.lnum - 4)
let regions[bufname] += [[entry.lnum, start, entry.lnum + 2]]
endif
else
" new context in new file
let start = (entry.lnum < 4) ? 0 : (entry.lnum - 4)
let regions[bufname] = [[entry.lnum, start, entry.lnum + 2]]
end
endfor
execute a:flags.side_cmd
" write error messages first
if !empty(errors)
call append('$', map(errors + [''], 's:error_marker . v:val'))
endif
" write contexts to buffer
for filename in sort(keys(regions))
let contexts = regions[filename]
let file = readfile(expand(filename))
let context = contexts[0]
call append('$', '>>> '. filename .':'. context[0])
call append('$', file[context[1]:context[2]])
for context in contexts[1:]
call append('$', ']]] '. filename .':'. context[0])
call append('$', file[context[1]:context[2]])
endfor
call append('$', '')
endfor
silent 1delete _
let nummatches = len(getqflist())
let numfiles = len(uniq(map(getqflist(), 'bufname(v:val.bufnr)')))
let &l:statusline = printf(' Found %d matches in %d files.', nummatches, numfiles)
endfunction
" s:side_buffer_settings() {{{2
function! s:side_buffer_settings() abort
nnoremap <silent><buffer> q :bdelete<cr>
nnoremap <silent><plug>(grepper-side-context-jump) :<c-u>call <sid>context_jump(1)
nnoremap <silent><plug>(grepper-side-context-open) :<c-u>call <sid>context_jump(0)
nnoremap <silent><plug>(grepper-side-context-next) :<c-u>call <sid>context_next()
nnoremap <silent><plug>(grepper-side-context-prev) :<c-u>call <sid>context_previous()
nmap <buffer> <cr> <plug>(grepper-side-context-jump)<cr>
nmap <buffer> o <plug>(grepper-side-context-open)<cr>
nmap <buffer> } <plug>(grepper-side-context-next)<cr>
nmap <buffer> { <plug>(grepper-side-context-prev)<cr>
setlocal buftype=nofile bufhidden=wipe nonumber norelativenumber foldcolumn=0
set nowrap
normal! zR
silent! normal! n
set conceallevel=2
set concealcursor=nvic
let b:grepper_side = s:filename_regexp
setfiletype GrepperSide
syntax match GrepperSideSquareBracket /]/ contained containedin=GrepperSideSquareBrackets conceal cchar=.
execute 'syntax match GrepperSideSquareBrackets /^]]] \v'.s:filename_regexp[20:].'/ conceal contains=GrepperSideSquareBracket'
syntax match GrepperSideAngleBracket /> \?/ contained containedin=GrepperSideFile conceal
execute 'syntax match GrepperSideFile /^>>> \v'.s:filename_regexp[20:].'/ contains=GrepperSideAngleBracket'
execute 'syntax match GrepperSideErrorMarker /^'.s:error_marker.'/ contained containedin=GrepperSideError conceal'
execute 'syntax match GrepperSideError /^'.s:error_marker.'.*/ contains=GrepperSideCaret'
highlight default link GrepperSideFile Directory
highlight default link GrepperSideSquareBrackets Conceal
highlight default link GrepperSideError ErrorMsg
endfunction
" s:side_context_next() {{{2
function! s:context_next() abort
call search(s:filename_regexp)
call s:side_context_scroll_into_viewport()
endfunction
" s:side_context_previous() {{{2
function! s:context_previous() abort
call search(s:filename_regexp, 'bc')
if line('.') == 1
$
call s:side_context_scroll_into_viewport()
else
-
endif
call search(s:filename_regexp, 'b')
endfunction
" s:side_context_scroll_into_viewport() {{{2
function! s:side_context_scroll_into_viewport() abort
redraw " needed for line('w$')
let next_context_line = search(s:filename_regexp, 'nW')
let current_line = line('.')
let last_line = line('$')
let last_visible_line = line('w$')
if next_context_line > 0
let context_length = (next_context_line - 1) - current_line
else
let context_length = last_line - current_line
endif
let scroll_length = context_length - (last_visible_line - current_line)
if scroll_length > 0
execute 'normal!' scroll_length."\<c-e>"
endif
endfunction
" s:side_context_jump() {{{2
function! s:context_jump(close_window) abort
let fileline = search(s:filename_regexp, 'bcn')
if empty(fileline)
return
endif
let [filename, line] = matchlist(getline(fileline), s:filename_regexp)[1:2]
if a:close_window
silent! close
execute 'edit +'.line fnameescape(filename)
else
wincmd p
execute 'edit +'.line fnameescape(filename)
wincmd p
endif
endfunction
" }}}1
" Operator {{{1
function! GrepperOperator(type) abort
let regsave = @@
let selsave = &selection
let &selection = 'inclusive'
if a:type =~? 'v'
silent execute "normal! gvy"
elseif a:type == 'line'
silent execute "normal! '[V']y"
else
silent execute "normal! `[v`]y"
endif
let &selection = selsave
let flags = s:get_config().operator
let flags.query_orig = @@
let flags.query_escaped = 0
let flags.query = s:escape_query(flags, @@)
if s:get_current_tool_name(flags) != 'findstr'
\ && !flags.buffer && !flags.buffers
let flags.query = '-- '. flags.query
endif
let @@ = regsave
return s:start(flags)
endfunction
" Mappings {{{1
nnoremap <silent> <plug>(GrepperOperator) :set opfunc=GrepperOperator<cr>g@
xnoremap <silent> <plug>(GrepperOperator) :<c-u>call GrepperOperator(visualmode())<cr>
if hasmapto('<plug>(GrepperOperator)')
silent! call repeat#set("\<plug>(GrepperOperator)", v:count)
endif
" Commands {{{1
command! -nargs=* -complete=customlist,grepper#complete Grepper call <sid>parse_flags(<q-args>)
for s:tool in g:grepper.tools
let s:utool = substitute(toupper(s:tool[0]) . s:tool[1:], '-\(.\)',
\ '\=toupper(submatch(1))', 'g')
execute 'command! -nargs=+ -complete=file Grepper'. s:utool
\ 'Grepper -noprompt -tool' s:tool '-query <args>'
endfor
" vim: tw=80 et sts=2 sw=2 fdm=marker