I need to build a quickfix list based on output from some external command. But the command gives me only file names and line numbers, like:
foo.txt:10
bar.txt:20
I'd like to add the actual contents of the specified file into the quickfix list, like in:
foo.txt:10: this is some line from foofile
bar.txt:20: hello world, we're a line from barfile
Can this be done?
Ideally, I'd like this to be cross-platform, so probably in pure VimScript, with no calls to external commands like sed or the likes?
Simulation
My current behavior can be simulated with a function like:
function! MyFunc()
let mylist = ["foo.txt:10:...", "bar.txt:20:..."]
cgetexpr mylist
copen
endfunction
call MyFunc()
and I'd like the ... parts to become the contents from the real files...
Here's a solution:
fun! GetFileLine(fn,ln)
return readfile(a:fn,'',a:ln)[a:ln-1]
endfun
fun! AppendLineToFnLn(list)
return map(a:list, 'v:val.'':''.call(''GetFileLine'', split(v:val,'':'') )' )
endfun
fun! QuickFixWithLine(cmd)
cexpr AppendLineToFnLn(split(system(a:cmd),"\n"))
endfun
call QuickFixWithLine('echo myfile:22; echo myfile:40;')
copen
Hmh, based on a partially related question on comp.editors and :help readfile, I'd say below might work, however wasteful:
function! MyFunc()
let mylist = ["foo.txt:10:...", "bar.txt:20:..."]
let result = []
for elem in mylist
let temp = split(elem, ":")
let line = elem . ":" . readfile(temp[0], "", temp[1])[temp[1]-1]
call add(result, line)
endfor
cgetexpr line
copen
endfunction
call MyFunc()
Related
How do I pass a visual selection to a function from a script and NOT command line / mapping. I was wondering if something like this was possible? Or if there is a function that gets a range ?
What I want:
:call LowerToUpper('<,'>)
:call LowerToUpper(GetVisualRange())
NOT THIS
:call LowerToUpper(1,5)
command! -range Edit call LowerToUpper(<line1>,<line2>)
:'<,'>LowerToUpper
Here is the function example:
function! LowerToUpper(first,last) abort
for lineno in range(a:first,a:last)
let line = getline(lineno)
let newLine= substitute(line, '\v(\w)','\U\1','g')
call setline(lineno,newLine)
endfor
endfunction
The solution might be a hack to make a function that returns the visual selection GetVisualSelection().
:call LowerToUpper('<,'>)
You're very very close. Use line() to get lines of the marks:
:call LowerToUpper(line("'<"), line("'>"))
You can have your function with range modifier.
function! LowerToUpper() abort range
for lineno in range(a:firstline, a:lastline)
let line = getline(lineno)
let newLine = substitute(line, '\v(\w)','\U\1','g')
call setline(lineno, newLine)
endfor
endfunction
'<,'>call LowerToUpper()
This mostly serves as a shortcut for passing line("'<") and line("'>") implicitly. The argument names a:firstline and a:lastline are fixed.
How can I make vim's :global command ask the user if they want to execute the ex command? Similar to what happens with the :substite command with the 'c' option, for example %s:Foo:Fighters:gc
I tried:
:g/mypattern/.s:.*\n::gc
and
:g/mypattern/s:.*\n::gc
but if there is a match on below line it is jumped. For example:
MATCH
NONMATCH
MATCH
MATCH
MATCH
The result is:
NONMATCH
MATCH <<-- this should be erased.
A promptable g/FOO/d would be perfect.
There is no native way to do this. The typical method would be to record a macro and repeat a macro. Making sure you n or / at the end of the macro to advance to the next match. Skipping is now simply n and ## to execute the macro.
Custom :Global command
However if you truly want to have :global command with a confirm you can sort of mimic this by using confirm() inside the your command. The general idea is to do something like this:
:g/pat/if confirm("&yes\n&no", 2) == 1 | cmd | endif
This doesn't quite work for the following reasons:
You have no idea where your cursor is. Need something like :match and :redraw
Does not abort well. Need a way to throw an exception to abort
Very unwieldily to type this all out
I have come up with the following confirming :Global/:G command
command! -nargs=+ -range=% -complete=command Global <line1>,<line2>call <SID>global_confirm(<q-args>)
command! -nargs=+ -range=% -complete=command G <line1>,<line2>call <SID>global_confirm(<q-args>)
function! s:global_confirm(args) range
let args = a:args
let sep = args[0]
let [pat, cmd; _] = split(args[1:], '\v([^\\](\\\\)*\\)#<!%d' . char2nr(sep), 1) + ['', '']
match none
let options = ['throw "Global: Abort"', cmd, '', 'throw "Global: Abort"']
let cmd = 'exe ''match IncSearch /\c\%''.line(''.'').''l''.#/.''/'''
let cmd .= '| redraw'
let cmd .= '| exe get(options, confirm("Execute?", "&yes\n&no\n&abort", 2))'
try
execute a:firstline . ',' . a:lastline . 'g'.sep.pat.sep.cmd
catch /Global: Abort/
finally
match none
endtry
endfunction
Note: Use as-is. Uses IncSearch for highlight and forces \c.
Now you can run :G/foo/d.
Custom :Confirm command
If you rather use a similar technique to the one #Randy Morris provided and use the following :Confirm {cmd} command to confirm {cmd} before execution.
command! -nargs=+ -complete=command Confirm execute <SID>confirm(<q-args>) | match none
function! s:confirm(cmd)
let abort = 'match none | throw "Confirm: Abort"'
let options = [abort, a:cmd, '', abort]
match none
execute 'match IncSearch /\c\%' . line('.') . 'l' . #/ . '/'
redraw
return get(options, confirm('Execute?', "&yes\n&no\n&abort", 2), abort)
endfunction
This will allow you to use :g/foo/Confirm d
For more help see:
:h #
:h q
:h confirm()
:h :exe
:h get()
:h :match
:h :redraw
As far as I know there is no way to do this natively. I think I've hacked together a way to do this but it's probably buggy as I haven't written vimscript in a long time. In this I've defined a command C which accepts an ex command as its arguments. Each line returned via :global is then passed to this ex command if you press y or Y. Any other key causes this line to be skipped.
let s:GlobalConfirmSignNumber = 42
sign define GlobalConfirmMarker text=>> texthl=Search
function GlobalConfirm(cmd)
let line = getpos(".")[1]
execute "sign place " . s:GlobalConfirmSignNumber . " line=" . line . " name=GlobalConfirmMarker file=" . expand("%:p")
redraw!
echomsg "Execute? (y/N) "
try
let char = nr2char(getchar())
if (char == "y" || char == "Y")
execute a:cmd
endif
finally
" Ensure signs are cleaned up if execution is aborted.
execute "sign unplace " . s:GlobalConfirmSignNumber
endtry
redraw!
endfunction
command -nargs=* C call GlobalConfirm(<q-args>)
Here's a gif of it in action. In this gif I'm running the command norm! gUU for every line which contains ba. In this case I confirmed every match by pressing y three times.
If anyone can make improvements to this (especially the signs bit) please edit at will.
Is there a function in Vim to get the list of all current mappings as a dictionary? (I know about :map command.)
If no (it seems so), is there a decent workaround?
Here is the practical motivation for this question: i would like to be able to clear all key mappings except those that start with <Plug>.
Nothing I could see either. You could use :redir to get the map output into a register and manipulate that. This snippet of vim script will do the job:
redir #a
silent map
redir END
let l = split(#a, '\n')
Now l contains a list of mappings, though parsing each entry might be a pain.
To filter these for those not containing <Plug> add this line:
call filter(l, 'v:val !~ "<Plug>"')
An example of a script to unmap non-<Plug> entries could be:
redir #a
silent map
redir end
let l = split(#a, '\n')
call filter(l, 'v:val !~ "<Plug>"')
for line in l
let type = line[0]
let thekey = split(line[1:len(line)], ' \+')[0]
try
exec type. 'unmap '. thekey
catch /E31/
endtry
endfor
I normally have quite a few buffers opened, which I navigate using combination of Bufexplorer and FuzzyFinder. Finding the right buffer still involves going through file names. But often, it could be much easier to say something like 'jump to buffer that contains "wip"'. Anyone knows how?
I am using a small function I put inside my .vimrc:
function! s:GrepOpenBuffers(search, jump)
call setqflist([])
let cur = getpos('.')
silent! exe 'bufdo vimgrepadd /' . a:search . '/ %'
let matches = len(getqflist())
if a:jump && matches > 0
sil! cfirst
else
call setpos('.', cur)
endif
echo 'BufGrep:' ((matches) ? matches : 'No') 'matches found'
endfunction
com! -nargs=1 -bang BufGrep call <SID>GrepOpenBuffers('<args>', <bang>0)
You could use something like the above to grep for a search term in all opened buffers.
Check out buffer grep: http://www.vim.org/scripts/script.php?script_id=2545
I have this function:
function! Find(name)
let l:list=system("find . -name '".a:name."' | perl -ne 'print \"$.\\t$_\"'")
let l:num=strlen(substitute(l:list, "[^\n]", "", "g"))
if l:num < 1
echo "'".a:name."' not found"
return
endif
if l:num != 1
echo l:list
let l:input=input("Which ? (CR=nothing)\n")
if strlen(l:input)==0
return
endif
if strlen(substitute(l:input, "[0-9]", "", "g"))>0
echo "Not a number"
return
endif
if l:input<1 || l:input>l:num
echo "Out of range"
return
endif
let l:line=matchstr("\n".l:list, "\n".l:input."\t[^\n]*")
else
let l:line=l:list
endif
let l:line=substitute(l:line, "^[^\t]*\t./", "", "")
execute ":e ".l:line
endfunction
command! -nargs=1 Find :call Find("<args>")
When I try adding a parameter, so the declaration becomes function! Find(name, search_dir), it always tells me i don't have enough parameters when I call the function in vim using :Find x y(where as Find: x would work when ther was only 1 parameter in the function declaration.
Any idea how I can add multiple parameters?
The end goal is to have a Find function that finds in a specified subdirectory.
An addition to #Peter Rincker answer: you should never use "<args>" if you want to pass parameter to a function as there is already a builtins <q-args> and <f-args> which do not need additional quoting and do not introduce a possibility of code injection (try Find ".string(g:)." with your code). First will pass all parameters as one item, second will produce a list of parameters suitable for a function call:
command -nargs=+ Find :call Find(<f-args>)
Another things to consider:
(system() call) Never pass user input to shell as-is, use shellescape(str, 1). You may have unexpected problems here.
strlen(substitute(l:input, "[0-9]", "", "g"))>0 condition is equivalent to input=~#'\D', but is much bigger.
You don't need to specify l:: it is the default scope inside functions.
There is a built-in glob() function: the whole system() line can be replaced with
join(map(split(glob('./*'.escape(a:name, '\`*[]?').'*'), "\n"), 'v:key."\t".v:val'), "\n")
Don't forget to escape everything you execute: execute ":e ".l:line should be written as execute "e" fnameescape(line) (it is the third place where code injection is possible in such a simple code snippet!).
It is better to use lists here, in this case you don't need to use something to add line numbers:
function s:Find(name)
let list=split(glob('./*'.fnameescape(a:name).'*'), "\n")
if empty(list)
echo string(a:name) "not found"
return
endif
let num=len(list)
if num>1
echo map(copy(list), 'v:key."\t".v:val')
let input=input("Which?")
if empty(input)
return
elseif input=~#'\D'
echo "Not a number"
return
elseif input<1 || input>num
echo "Out of range"
return
endif
let line=list[input]
else
let line=list[0]
endif
execute "e" fnameescape(line)
endfunction
command! -nargs=1 Find :call Find(<f-args)
Neither you nor me handle the situation where filename that matches pattern contains a newline. I know how to handle this (you can see my vim-fileutils plugin (deprecated) or os.listdir function of os module of frawor plugin (in alpha stage, not posted to vim.org)). I don't think such situation is likely, so just remember that it is possible.
You need to change -nargs=1 to -nargs=+. This will mean you have to have arguments but does not specify a number. I suggest you change your Find function to Find(...) and use a:0 get the number of arguments to error out if an invalid number of arguments are used.
Example function and command with multiple parameters:
command! -nargs=+ -complete=dir Find call Find(<f-args>)
fun! Find(name, ...)
let dir = getcwd()
if a:0 == 1
let dir = getcwd() . '/' . (a:1 =~ '[/\\]$' ? a:1 : a:1 . '/')
elseif a:0 != 0
echohl ErrorMsg
echo "Must supply 1 or 2 arguments"
echohl NONE
endif
let &efm = "%f"
cexpr []
caddexpr split(glob(dir . '**/*' . escape(a:name, '\`*[]?') . '*'), '\n')
copen
aug Find
au!
au BufLeave <buffer> ccl|aug! Find
aug END
endfun
For more help
:h :command-nargs
:h ...
Edit
Added example of function that excepts multiple parameters as suggest by #ZyX.