Lua stdlib feature parity against Vimscript
Tracking issue to collect the current status of Lua stdlib against Vimscript.
As all functions listed here are accessible in Lua via vim.fn.*, the goal is not to achieve 100% parity, however the typval conversion may have perf concerns and/or functional issues for specific functions so a Lua equivalent may be desirable. Also some vimscript functions aren't particularly relevant in the Lua domain.
I've filled the table to the best of my current knowledge. I there are any mistakes/disagreements/omissions then please comment.
| Total | 412 |
|---|---|
| Equivalent exists :white_check_mark: | 155 |
| Not required :heavy_minus_sign: | 50 |
| Wanted :grey_exclamation: | 5 |
| Uncategorised | 204 |
Functions
Math functions (29/29 :white_check_mark:)
| Vimscript function | Lua equivalent / Notes | Equiv exists(:white_check_mark:)/wanted(:grey_exclamation:) /not required(:heavy_minus_sign:) |
|---|---|---|
abs |
math.abs |
:white_check_mark: |
acos |
math.acos |
:white_check_mark: |
and |
bit.band |
:white_check_mark: *1 |
asin |
math.asin |
:white_check_mark: |
atan2 |
math.atan2 |
:white_check_mark: |
atan |
math.atan |
:white_check_mark: |
ceil |
math.ceil |
:white_check_mark: |
cos |
math.cos |
:white_check_mark: |
cosh |
math.cosh |
:white_check_mark: |
exp |
math.exp |
:white_check_mark: |
float2nr |
math.floor(x+0.5) |
:white_check_mark: |
floor |
math.floor |
:white_check_mark: |
fmod |
math.fmod |
:white_check_mark: |
invert |
bit.bnot |
:white_check_mark: *1 |
log10 |
math.log10 |
:white_check_mark: |
log |
math.log |
:white_check_mark: |
max |
math.max |
:white_check_mark: |
min |
math.min |
:white_check_mark: |
or |
bit.bor |
:white_check_mark: *1 |
pow |
math.pow |
:white_check_mark: |
rand |
math.random, vim.loop.random |
:white_check_mark: |
round |
math.floor(x+0.5) |
:white_check_mark: |
sin |
math.sin |
:white_check_mark: |
sinh |
math.sinh |
:white_check_mark: |
sqrt |
math.sqrt |
:white_check_mark: |
srand |
math.randomseed |
:white_check_mark: |
tan |
math.tan |
:white_check_mark: |
tanh |
math.tanh |
:white_check_mark: |
xor |
bit.bxor |
:white_check_mark: *1 |
*1: Not yet available with PUC Lua (#21222)
| Vimscript function | Lua equivalent / Notes | Equiv exists(:white_check_mark:)/wanted(:grey_exclamation:) /not required(:heavy_minus_sign:) |
|---|---|---|
add |
table.insert |
:white_check_mark: |
api_info |
vim.api.nvim_get_api_info |
:white_check_mark: |
append |
vim.api.nvim_buf_set_lines |
:white_check_mark: |
appendbufline |
vim.api.nvim_buf_set_lines |
:white_check_mark: |
argc |
||
argidx |
||
arglistid |
||
argv |
||
assert_beeps |
:heavy_minus_sign: | |
assert_equal |
:heavy_minus_sign: | |
assert_equalfile |
:heavy_minus_sign: | |
assert_exception |
:heavy_minus_sign: | |
assert_fails |
:heavy_minus_sign: | |
assert_false |
assert(not value) |
:heavy_minus_sign: |
assert_inrange |
:heavy_minus_sign: | |
assert_match |
:heavy_minus_sign: | |
assert_nobeep |
:heavy_minus_sign: | |
assert_notequal |
:heavy_minus_sign: | |
assert_notmatch |
:heavy_minus_sign: | |
assert_report |
:heavy_minus_sign: | |
assert_true |
assert |
:heavy_minus_sign: |
browse |
Does nothing in Neovim | :heavy_minus_sign: |
browsedir |
Does nothing in Neovim | :heavy_minus_sign: |
bufadd (to create bufs) |
vim.api.nvim_create_buf |
:white_check_mark: |
bufexists |
vim.api.nvim_buf_is_valid |
:white_check_mark: |
buflisted |
||
bufload |
||
bufloaded |
vim.api.nvim_buf_is_loaded |
:white_check_mark: |
bufname |
vim.api.nvim_buf_get_name |
:white_check_mark: |
bufnr |
||
bufnr() |
vim.api.nvim_get_current_buf |
:white_check_mark: |
bufwinid |
Easy to implement (see below) | :heavy_minus_sign: |
bufwinnr |
||
byte2line |
||
byteidx |
||
byteidxcomp |
||
call |
NA (typval) | :heavy_minus_sign: |
chanclose |
||
changenr |
||
chansend |
vim.api.nvim_chan_send |
:white_check_mark: |
char2nr |
string.byte *only works with ASCII |
:white_check_mark: |
charcol |
||
charidx |
||
chdir |
vim.api.nvim_set_current_dir |
:white_check_mark: |
cindent |
:heavy_minus_sign: | |
clearmatches |
||
col |
||
complete_add |
||
complete_check |
||
complete_info |
||
complete |
||
confirm |
vim.ui.select |
:white_check_mark: |
copy |
{table.unpack(obj)} |
:white_check_mark: |
count |
||
ctxget(0) |
vim.api.nvim_get_context |
:white_check_mark: |
ctxget |
||
ctxpop |
||
ctxpush |
||
ctxset |
||
ctxsize |
||
cursor |
vim.api.nvim_win_set_cursor |
:white_check_mark: |
debugbreak |
||
deepcopy |
vim.deepcopy |
:white_check_mark: |
delete |
||
deletebufline |
vim.api.nvim_buf_set_lines |
:white_check_mark: |
dictwatcheradd |
__index metamethod + autocmd |
:heavy_minus_sign: |
dictwatcherdel |
__index metamethod + autocmd |
:heavy_minus_sign: |
did_filetype |
||
diff_filler |
||
diff_hlID |
Obscure | :heavy_minus_sign: |
digraph_get{,list} |
||
digraph_set{,list} |
||
empty |
vim.tbl_isempty |
:white_check_mark: |
environ |
vim.loop.uv.os_environ |
:white_check_mark: |
escape |
||
eval |
loadstring |
:white_check_mark: |
eventhandler |
:heavy_minus_sign:? | |
executable |
||
execute |
vim.api.nvim_exec, vim.cmd |
:white_check_mark: |
exepath |
:white_check_mark: | |
exists |
||
expand |
||
expandcmd |
||
extend |
vim.list_extend, vim.tbl_extend |
:white_check_mark: |
feedkeys |
vim.api.nvim_feedkeys |
:white_check_mark: |
filereadable |
vim.loop.fs_stat |
:white_check_mark: |
filewritable |
vim.loop.fs_stat |
:white_check_mark: |
filter |
vim.tbl_filter |
:white_check_mark: |
finddir |
||
findfile |
vim.fs.find |
:white_check_mark: |
flatten |
vim.tbl_flatten |
|
fnameescape |
||
fnamemodify |
||
foldclosed |
||
foldclosedend |
||
foldlevel |
||
foldtext |
||
foldtextresult |
||
foreground |
||
fullcommand |
||
funcref |
NA (typval) | :heavy_minus_sign: |
function |
NA (typval) | :heavy_minus_sign: |
garbagecollect |
:heavy_minus_sign: | |
get |
lua <table>.<key_or_idx> or <default> |
:white_check_mark: |
getbufinfo |
vim.api.nvim_list_bufs + vim.api.nvim_buf_* |
:white_check_mark: |
getbufline |
||
getbufvar |
vim.api.nvim_buf_get_var, vim.b[idx][name] |
:white_check_mark: |
getchangelist |
||
getchar |
||
getcharmod |
||
getcharpos |
||
getcharsearch |
||
getcharstr |
||
getcmdline |
||
getcmdpos |
||
getcmdtype |
||
getcmdwintype |
:grey_exclamation: (#10735) | |
getcompletion |
||
getcurpos |
||
getcursorcharpos |
||
getcwd |
vim.loop.cwd *does not work for window/tab local |
:white_check_mark: |
getenv |
vim.loop.os_getenv, os.getenv |
:white_check_mark: |
getfontname |
||
getfperm |
vim.loop.fs_stat(...).mode |
:white_check_mark: |
getfsize |
vim.loop.fs_stat(...).size |
:white_check_mark: |
getftime |
vim.loop.fs_stat(...).mtime |
:white_check_mark: |
getftype |
vim.loop.fs_stat(...).type |
:white_check_mark: |
getjumplist |
||
getline |
vim.api.nvim_buf_get_lines |
:white_check_mark: |
getloclist |
||
getmarklist |
vim.api.nvim_get_mark |
:white_check_mark: |
getmatches |
||
getmousepos |
||
getpid |
vim.loop.os_getpid |
:white_check_mark: |
getpos('.') |
vim.api.nvim_win_get_cursor |
|
getpos |
||
getqflist |
||
getreg |
||
getreginfo |
||
getregtype |
||
gettabinfo |
||
gettabvar |
vim.api.nvim_tab_get_var, vim.t[idx][name] |
:white_check_mark: |
gettabwinvar |
vim.api.nvim_win_get_var, vim.w[idx][name] |
:white_check_mark: |
gettagstack |
||
getwininfo |
Partially with #15248 | :grey_exclamation: |
getwinpos |
||
getwinposx |
||
getwinposy |
||
getwinvar |
vim.api.nvim_win_get_var, vim.w[idx][name] |
:white_check_mark: |
glob2regpat |
||
glob |
||
globpath |
||
has_key |
<table>[key] ~= nil |
:white_check_mark: |
has |
||
haslocaldir |
||
hasmapto |
||
histadd |
||
histdel |
||
histget |
||
histnr |
||
hlID |
vim.api.nvim_get_hl_id_by_name |
:white_check_mark: |
hlexists |
vim.api.nvim_get_hl_* |
:white_check_mark: |
hostname |
vim.loop.os_gethostname() |
:white_check_mark: |
iconv |
vim.iconv |
:white_check_mark: |
id |
NA (typval) | :heavy_minus_sign: |
indent |
||
index |
||
input |
vim.ui.input |
:white_check_mark: |
inputlist |
vim.ui.select |
:white_check_mark: |
inputrestore |
||
inputsave |
||
inputsecret |
||
insert |
||
interrupt |
No args or return so no typval conversion | :heavy_minus_sign: |
isdirectory |
vim.loop.fs_stat(...).type == 'directory' |
:white_check_mark: |
isinf |
Easy to implement, see below | :heavy_minus_sign: |
islocked |
:heavy_minus_sign: | |
isnan |
Easy to implement, see below | :heavy_minus_sign: |
items |
pairs |
:white_check_mark: |
jobpid |
vim.loop.process_get_pid |
:white_check_mark: |
jobresize |
||
jobstart |
vim.loop.spawn |
:white_check_mark: |
jobstop |
vim.loop.shutdown and vim.loop.close |
:white_check_mark: |
jobwait |
||
join |
table.concat |
:white_check_mark: |
json_decode |
vim.json.decode |
:white_check_mark: |
json_encode |
vim.json.encode |
:white_check_mark: |
keys |
vim.tbl_keys |
:white_check_mark: |
len |
Lua # operator, #vim.tbl_keys() |
:white_check_mark: |
libcall, libcallnr |
package.loadlib, ffi.load | :white_check_mark: |
line2byte |
||
line |
||
lispindent |
||
list2str |
vim.inspect, table.concat |
:white_check_mark: |
localtime |
vim.loop.gettimeofday |
:white_check_mark: |
luaeval |
loadstring | :white_check_mark: |
map |
vim.tbl_map |
:white_check_mark: |
maparg |
||
mapcheck |
||
match |
vim.regex with regex:match_str |
:white_check_mark: |
matchadd |
||
matchaddpos |
Extmarks | :white_check_mark: |
matcharg |
||
matchdelete |
||
matchend |
||
matchfuzzy |
||
matchfuzzypos |
||
matchlist |
||
matchstr |
||
matchstrpos |
||
menu_get |
||
mkdir |
vim.loop.fs_mkdir |
:white_check_mark: |
mode |
vim.api.nvim_get_mode |
:white_check_mark: |
msgpackdump |
vim.mpack.encode |
:white_check_mark: |
msgpackparse |
vim.mpack.decode |
:white_check_mark: |
nextnonblank |
||
nr2char |
string.char *only works with ASCII |
:white_check_mark: |
pathshorten |
||
perleval |
||
prevnonblank |
||
printf |
print(string.format(...)) |
:white_check_mark: |
prompt_getprompt |
||
prompt_setcallback |
||
prompt_setinterrupt |
||
prompt_setprompt |
||
pum_getpos |
||
pumvisible |
||
py3eval |
||
pyeval |
||
pyxeval |
||
range |
Various ways depending on context | :white_check_mark: |
readdir |
||
readfile |
vim.loop.fs_read |
:white_check_mark: |
reduce |
||
reg_executing |
||
reg_recorded |
||
reg_recording |
||
reltime[,float,str] |
vim.loop.hrtime, vim.loop.gettimeofday |
:white_check_mark: |
remove |
os.remove |
:white_check_mark: |
rename |
vim.loop.fs_rename, os.rename |
:white_check_mark: |
repeat |
string.rep |
:white_check_mark: |
resolve |
vim.loop.fs_readlink |
:white_check_mark: |
reverse |
Easy to implement, see below | :heavy_minus_sign: |
rpcnotify |
vim.rpcnotify |
:white_check_mark: |
rpcrequest |
vim.rpcrequest |
:white_check_mark: |
rpcstart |
||
rubyeval |
||
screenattr |
||
screenchar |
||
screenchars |
||
screencol |
||
screenpos |
||
screenrow |
||
screenstring |
||
search |
||
searchcount |
||
searchdecl |
||
searchpair |
||
searchpairpos |
||
searchpos |
||
serverlist |
Covered by vim.loop.*? | :heavy_minus_sign: |
serverstart |
Covered by vim.loop.*? | :heavy_minus_sign: |
serverstop |
Covered by vim.loop.*? | :heavy_minus_sign: |
setbufline |
vim.api.nvim_buf_set_lines |
:white_check_mark: |
setbufvar |
vim.api.nvim_buf_set_var, vim.b[idx][name] |
:white_check_mark: |
setcharpos |
||
setcharsearch |
||
setcmdpos |
||
setcursorcharpos |
||
setenv |
vim.loop.os_setenv |
:white_check_mark: |
setfperm |
vim.loop.fs_chmod |
:white_check_mark: |
setline |
vim.api.nvim_buf_set_lines |
:white_check_mark: |
setloclist |
||
setmatches |
||
setpos |
||
setqflist |
||
setreg |
||
settabvar |
vim.api.nvim_tabpage_set_var, vim.t[idx][name] |
:white_check_mark: |
settabwinvar |
||
settagstack |
||
setwinvar |
vim.api.nvim_win_set_var, vim.w[idx][name] |
:white_check_mark: |
sha256 |
Typval conversion is costly | :grey_exclamation: |
shellescape |
||
shiftwidth |
||
sign_define |
extmarks | :heavy_minus_sign: |
sign_getdefined |
extmarks | :heavy_minus_sign: |
sign_getplaced |
extmarks | :heavy_minus_sign: |
sign_jump |
extmarks | :heavy_minus_sign: |
sign_place |
extmarks | :heavy_minus_sign: |
sign_placelist |
extmarks | :heavy_minus_sign: |
sign_undefine |
extmarks | :heavy_minus_sign: |
sign_unplace |
extmarks | :heavy_minus_sign: |
sign_unplacelist |
extmarks | :heavy_minus_sign: |
simplify |
||
sockconnect |
Should be covered in vim.loop.* |
:heavy_minus_sign: |
sort |
table.sort |
:white_check_mark: |
soundfold |
||
spellbadword |
vim.spell.check |
:white_check_mark: |
spellsuggest |
||
split |
vim.split |
:white_check_mark: |
stdioopen |
||
stdpath |
||
str2float |
tonumber |
:white_check_mark: |
str2list |
vim.inspect, table.concat |
:white_check_mark: |
str2nr |
tonumber |
:white_check_mark: |
strcharpart |
string.sub |
:white_check_mark: |
strchars |
string.len |
:white_check_mark: |
strdisplaywidth |
||
strftime |
date.format |
:white_check_mark: |
strgetchar |
string.sub |
:white_check_mark: |
stridx |
||
string |
tostring |
:white_check_mark: |
strlen |
string.len or Lua # operator | :white_check_mark: |
strpart |
||
strptime |
||
strridx |
string.find |
:white_check_mark: |
strtrans |
||
strwidth |
vim.api.nvim_strwidth |
:white_check_mark: |
submatch |
||
substitute |
vim.gsub (Lua patterns instead of regex) |
:white_check_mark: |
swapinfo |
||
swapname |
||
synID |
||
synIDattr |
||
synIDtrans |
||
synconcealed |
||
synstack |
||
system |
vim.loop.spawn |
:white_check_mark: |
systemlist |
vim.loop.spawn |
:white_check_mark: |
tabpagebuflist |
||
tabpagenr |
vim.api.nvim_get_current_tabpage |
:white_check_mark: |
tabpagewinnr |
||
tagfiles |
||
taglist |
||
tempname |
vim.loop.fs_mktemp, os.tmpname |
:white_check_mark: |
termopen |
||
test_garbagecollect_now |
:heavy_minus_sign: | |
test_write_list_log |
:heavy_minus_sign: | |
timer_info |
vim.loop.timer_{get_repeat,get_due_in} |
:white_check_mark: |
timer_pause |
||
timer_start |
vim.loop.timer_start |
:white_check_mark: |
timer_stop |
vim.loop.timer_stop |
:white_check_mark: |
timer_stopall |
No args or return so no typval conversion | :heavy_minus_sign: |
tolower |
string.lower |
:white_check_mark: |
toupper |
string.upper |
:white_check_mark: |
tr |
||
trim |
vim.trim |
:white_check_mark: |
trunc |
||
type |
NA (typval) | :heavy_minus_sign: |
undofile |
||
undotree |
||
uniq |
:grey_exclamation: | |
values |
vim.tbl_values |
:white_check_mark: |
virtcol |
||
visualmode |
||
wait |
vim.wait |
:white_check_mark: |
wildmenumode |
||
win_execute |
vim.api.nvim_win_call |
:white_check_mark: |
win_findbuf |
||
win_getid |
||
win_getid() |
vim.api.nvim_get_current_win |
:white_check_mark: |
win_gettype |
||
win_gotoid |
vim.api.nvim_set_current_win |
:white_check_mark: |
win_id2tabwin |
||
win_id2win |
||
win_move_separator |
vim.api.nvim_win_set_width |
:heavy_minus_sign: |
win_move_statusline |
||
win_screenpos |
vim.api.nvim_win_get_position |
:white_check_mark: |
win_splitmove |
||
winbufnr |
vim.api.nvim_win_get_buf |
:white_check_mark: |
wincol |
||
windowsversion |
||
winheight |
vim.api.nvim_win_get_height |
:white_check_mark: |
winlayout |
||
winline |
||
winnr('$') ('#') ... |
||
winnr() |
vim.api.nvim_win_get_number |
:white_check_mark: |
winrestcmd |
||
winrestview |
||
winsaveview |
||
winwidth |
vim.api.nvim_win_get_width |
:white_check_mark: |
wordcount |
Typval conv is negligible | :heavy_minus_sign: |
writefile |
vim.loop.fs_write |
:white_check_mark: |
Commands
| Vimscript command | Lua equivalent / Notes | Equiv exists(:white_check_mark:)/wanted(:grey_exclamation:) /not required(:heavy_minus_sign:) |
|---|---|---|
abbrev (and variants) |
:grey_exclamation: | |
augroup |
vim.api.nvim_create_augroup |
:white_check_mark: |
autocmd |
vim.api.nvim_create_autocmd |
:white_check_mark: |
colorscheme |
||
highlight |
vim.api.nvim_set_hl |
:white_check_mark: |
set (and variants) |
vim.api.nvim_set_option_value |
:white_check_mark: |
sign |
Extmarks API | :heavy_minus_sign: |
map |
vim.keymap |
:white_check_mark: |
command |
vim.api.nvim_create_user_command |
:white_check_mark: |
Lua equivalent snippets
bufwinid()
NOTE: This works for all tabpages, unlike vim.fn.bufwinid() which only works for the current tabpage.
Expand
local function bufwinid(buf)
for _, w in ipairs(vim.api.nvim_list_wins()) do
if vim.api.nvim_win_get_buf(w) == buf then
return w
end
end
return -1
end
reverse()
Expand
function reverse(t)
local n = #t
local i = 1
for i = 1, n do
t[i],t[n] = t[n],t[i]
n = n - 1
end
end
isinf()
Expand
local function isinf(value)
return value == math.huge and 1 or value == -math.huge and -1 or 0
end
isnan()
Expand
local function isnan(value)
return value ~= value
end
Other Notes:
- Does typconv conversion carry a heavy cost compared to the rest of the function execution? It may if the function can input large objects for it arguments or return values, or if the function is expected to be run in hot loops.
- Vimscript doesn't have proper booleans, so any vimscript function which should return boolean woud probably benefit from a Lua equivalent.
qq on bit.*: is that available when running neovim with puc lua?
qq on
bit.*: is that available when running neovim with puc lua?
https://github.com/neovim/neovim/issues/11352
Strictly speaking, nvim_win_get_cursor() cannot get curswant.
Strictly speaking,
nvim_win_get_cursor()cannot getcurswant.
Fixed. Added pos('.') which maps to nvim_win_get_cursor
vim.loop.exepath is not equivalent to Vim script's exepath. The former returns the full path to the current executable (i.e. nvim) while the latter takes an argument and returns the path to that, if it exists.
Also vim.input should be vim.ui.input.
vim.loop.exepathis not equivalent to Vim script'sexepath. The former returns the full path to the current executable (i.e. nvim) while the latter takes an argument and returns the path to that, if it exists.Also
vim.inputshould bevim.ui.input.
Done. Feel free to edit directly (I think you have permissions).
Is the table in the OP missing cnoreabbrev that is available in vimscript?
I have the following in my init.lua wrapped inside a vim.cmd[[]].
" Replace :w with :up
cnoreabbrev <expr> w getcmdtype() == ":" && getcmdline() == 'w' ? 'up' : 'w'
cnoreabbrev <expr> h getcmdtype() == ":" && getcmdline() == 'h' ? 'tab help' : 'h'
cnoreabbrev <expr> help getcmdtype() == ":" && getcmdline() == 'help' ? 'tab help' : 'help'
cnoreabbrev <expr> helpgrep getcmdtype() == ":" && getcmdline() == 'helpgrep' ? 'tab helpgrep' : 'helpgrep'
cnoreabbrev <expr> Man getcmdtype() == ":" && getcmdline() == 'Man' ? 'tab Man' : 'Man'
cnoreabbrev is a command, not a function and isn't included in vim.fn.*.
However, your point is valid that there is no Lua analogue for these kinds of Vimscript commands.
string.byte() and string.char() only work with ASCII. nvim_open_term is completely different from termopen().
I don't think stdpath and nvim_get_runtime_file are really equivalents.
stdpath is used to get the root of one of the standard paths (cache, config, etc.) while nvim_get_runtime_file is used to search for files under any of the runtime paths.
For example, it is impossible to get the path to cache with nvim_get_runtime_file.
vim.loop.cwd() cannot be used as a direct replacement for vim.fn.getcwd(), as it does not support arguments as vim.fn.getcwd() does, specifically being able to retrieve the cwd local to a window or tab.
bufadd is not completely equivalent to vim.api.nvim_create_buf , bufadd() to can be use to create a named buffers or get the bufnr of a named buffer (let g:foo_nr = bufadd('./foo.lua')) while nvim_create_buf can only create unnamed/scratch buffers, whats even worse is that :badd and bufadd() don't even have the same behavior, :badd will list the unloaded buffer while bufadd() wont
Please avoid comments about "incomplete equivalence of A to B". 100% fidelity is not the point of this tracking issue, we are trying to figure out API surface area. The details of one-to-one equivalence will be a task to address per-function, after we have a general idea of the major gaps.
Do we have a Lua API to turn off
syntaxfor a specific buffer?
vim.bo[bufnr].syntax = ''
Please do not clutter the issue tracker with user questions. There are other forums for this.
Please avoid comments about "incomplete equivalence of A to B". 100% fidelity is not the point of this tracking issue, we are trying to figure out API surface area. The details of one-to-one equivalence will be a task to address per-function, after we have a general idea of the major gaps.
FWIW, I don't mind people reporting differences between the reported mappings above. Even though 100% fidelity isn't a goal, these differences are useful to know about (or some of them are). Not to mention some of the equivalents I listed were completely wrong.
I've updated the OP and hidden all the comments.
@lewis6991 Do we have the Lua equivalent of :checktime {filename}?
Do we have tabdo in Lua?
Do we have
tabdoin Lua?
There's nvim_buf_call() and nvim_win_call() but no nvim_tabpage_call(), you can still use a for loop with nvim_list_tabpages() to iterate over tabs and then use nvim_tabpage_list_wins() to perform any action on the local windows
There is not a complete feature parity with printf function. string.format does not support positional arguments, whereas printf does.
Also see this stackoverflow question.