vimscript call vs. execute - vim

In vimscript, what is the difference between call and execute? In what scenarios / use cases should I use one vs the other?
(Disclaimer, I am aware of the extensive online help available within vim - I am seeking a concise answer to this specific question).

:call: Call a function.
:exec: Executes a string as an Ex command.
It has the similar meaning of eval(in javascript, python, etc)
For example:
function! Hello()
echo "hello, world"
endfunction
call Hello()
exec "call Hello()"

From the experience of writing my own plugins and reading the code of others:
:call is for calling functions, e.g.:
function! s:foo(id)
execute 'buffer' a:id
endfunction
let target_id = 1
call foo(target_id)
:execute is used for two things:
Construct a string and evaluate it. This is often used to pass arguments to commands:
execute 'source' fnameescape('l:path')
Evaluate the return value of a function (arguably the same):
function! s:bar(id)
return 'buffer ' . a:id
endfunction
let target_id = 1
execute s:bar(target_id)

Short answer
You may see call as first evaluate the expression and then discard the result. So only the side effects are useful.
Long answer
Define:
function! Foo()
echo 'echoed'
return 'returned'
endfunction
Call:
:call Foo()
Output:
echoed
Execute:
:execute Foo()
Output:
echoed
EXXX: Not an editor command: returned
Execute:
:silent let foo = Foo()
:echo foo
Output:
returned

See Switch to last-active tab in VIM
for example
:exe "tabn ".g:lasttab
Where
g:lasttab is a global variable to store the current tab number
and that number is concatenated with "tabnext" to switch e.g to tab number 3
(If g:lasttab e.g. contains '3' for example)
That whole string >"tabn ".g:lasttab<
is evaluated and executed by VIM's exec command.
HTH?

Related

Vim how to leave cursor at the end of the line after autocommand

I'm trying to make my own snippets in pure vimscript using autocommands. I want to make it so that after I type a specific character sequence, it is replaced by another character sequence. I made one like this, that replaces "hello" with "bye" when you type it.
function F()
if (strpart (getline('.'), 0, col('.')-1) =~ 'hello$')
execute "normal 5i\<Backspace>"
normal! abye
normal! l
endif
endfunction
autocmd TextChangedI *.tex call F()
And it works fine if there are characters after the cursor. However, if I am writing at the end of the line, after the change the cursor is between 'y' and 'e', because the autocommand calls the function, the cursor is at the end of the line, and then it enters insert mode which starts inserting before the last character.
How can I make it so that the cursor is always left after 'bye'? I don't want to use iabbrev for a couple of reasons, one being that it doesn't expand as soon as I type.
I can do it with the option
set ve+=onemore
but I don't like it's effects on normal writing.
How about the following, which uses setline and cursor functions rather than normal keymaps:
function! F() abort
let [l:line, l:col] = [getline('.'), col('.')]
if strpart(l:line, 0, l:col-1) =~ 'hello$'
let l:left = strpart(l:line, 0, l:col-6)
let l:right = strpart(l:line, l:col-1)
call setline('.', l:left . 'bye' . l:right)
call cursor('.', l:col-2) " 2 = length diff between hello and bye
endif
endfunction
This seems working for me (on Neovim 0.6).

How to simplify this pasting calculator

I have implemented a command in vim which pastes the result of a calculation into your file, i.e. you type
:CalP 34 * 89
and it should paste the result after your cursor.
The code is as follows:
command! -nargs=+ CalP :call Calculator(<q-args>) | normal! p
py from math import *
fun Calculator(arg)
redir #"
execute "py print " a:arg
redir END
let #" = strpart(#", 1)
endfun
This works but is messier than I would like for a simple operation, mainly because:
I don't know a better way to redirect the output of py print ... to the " register
I have to write execute "py print " a:arg because just py print a:arg doesn't work
The let #" = strpart(#", 1) removes the stray newline at the front of the register which py print creates, ideally this should be removed
I think this should be do-able in one line but I don't know enough vimscript.
No scripting is needed for this. In insert mode, you can use <Ctrl-R>=34*89<CR> to insert the result of that calculation.
:help i_CTRL-R
:help expression
I'll second #Amadan's suggestion. If you prefer Python over Vimscript, you can use the pyeval() function, e.g. directly from insert mode:
<C-R>=pyeval('34 * 89')<CR>
If you would like to keep your custom command, that's possible, too:
command! -nargs=+ CalP execute 'normal! a' . pyeval(<q-args>) . "\<Esc>"

How to clear output of function call in VIM?

I know my question title is not explanatory enough so let me try to explain.
I created a vim function that displays my current battery state. My function is as follows:
function! BatteryStatus()
let l:status = system("~/battery_status.sh")
echo join(split(l:status))
endfunction
I have mapped the above function as nnoremap <F3> :call BatteryStatus()<cr>. Now, when I press F3 it displays my battery status as Discharging, 56%, 05:01:42 remaining which is my required output but my question is how do I make the above output disappear.
Currently what happens is after function call it continuously displays the output and I have to manually use :echo to clear the command window(:).
So, what necessary changes are to be made in my function so that I can achieve toggle like behaviour.
battery_status.sh
acpi | awk -F ": " '{print $2}'
PS: This is part of a learning exercise. So, please don't suggest alternative vim scripts.
Simplistic straightforward way to toggling the output:
let s:battery_status_output_flag = "show"
function! BatteryStatus()
if s:battery_status_output_flag == "show"
let l:status = system("~/battery_status.sh")
echo join(split(l:status))
let s:battery_status_output_flag = "clear"
else
echo ""
let s:battery_status_output_flag = "show"
endif
endfunction
Note s: prefix, see :help script-variable
You can define a autocmd:
:au CursorHold * redraw!
Vim redraws itself 4 sec (set by updatetime option) after it's idle.
vim
function! pseudocl#render#clear()
echon "\r\r"
echon ''
endfunction
You can just Ctrl+ L to clear the message on the status
line.

modifying a vim function constantly giving me: "Not enough arguments for function"

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.

Vim: Call an ex command (set) from function?

Drawing a blank on this, and google was not helpful.
Want to make a function like this:
function JakPaste()
let tmp = :set paste?
if tmp == "paste"
set nopaste
else
set paste
endif
endfunction
map <F2> :call JakPaste()<CR>
However this does not work. I have isolated the broken line:
function JakPaste()
let tmp = set paste?
endfunction
map <F2> :call JakPaste()<CR>
Pressing F2 results in this error:
Error detected while processing function JakPaste:
line 1:
E121: Undefined variable: set
E15: Invalid expression: set paste?
Hit ENTER or type command to continue
How should I call an ex command (set) from a vim function?
This seems somewhat relevant however I still don't get it.
The reason this doesn't work is that you're trying to run a command in an expression - those are different things. The ? construct you used just causes vim to echo the value of the option; this isn't the same as a function returning the value. To be clear: the problem isn't that you're calling an ex command from a function - every other line of the function is an ex command - it's that you're calling the ex command in an expression.
But that's not the right way to carry out the task you're attempting here. Here's the shortest way, thanks to rampion's comment:
set paste!
Now, if you ever need something smarter than just inverting a boolean, you can use & to turn an option name into a usable variable. Here are two ways to use that:
" still a function, good for taking extra action (here, printing notification)"
function TogglePaste()
if (&paste)
set nopaste
echo "paste off"
else
set paste
echo "paste on"
endif
endfunction
" just set the variable directly"
let &paste = !&paste

Resources