The vim.fn module provides access to all Vimscript functions from Lua. It automatically converts
between Vim types and Lua types, making it seamless to call legacy Vimscript functions.
Overview
-- Call any Vimscript function
local line_count = vim.fn.line('$')
local file_exists = vim.fn.filereadable('init.lua')
local expanded = vim.fn.expand('%:p')
vim.fn converts directly between Vim and Lua objects. Floats become Lua numbers, empty lists/dicts become empty tables.
Type Conversion
Vim to Lua
| Vim Type | Lua Type |
|---|
| Number | number |
| Float | number |
| String | string |
| List | table (array) |
| Dict | table (key-value) |
v:true / v:false | true / false |
v:null | vim.NIL |
Lua to Vim
vim.fn.setline(1, 'Hello') -- String -> String
vim.fn.append(1, { 'a', 'b', 'c' }) -- Table -> List
vim.fn.extend({ a = 1 }, { b = 2 }) -- Table -> Dict
Common Functions
File Operations
-- Check if file exists
if vim.fn.filereadable('~/.vimrc') == 1 then
print('vimrc found')
end
-- Check if directory exists
if vim.fn.isdirectory('~/.config/nvim') == 1 then
print('Config directory exists')
end
-- Get file modification time
local mtime = vim.fn.getftime('init.lua')
-- Get file size
local size = vim.fn.getfsize('init.lua')
-- Read file to list
local lines = vim.fn.readfile('init.lua')
for _, line in ipairs(lines) do
print(line)
end
-- Write file from list
vim.fn.writefile({ 'line1', 'line2' }, '/tmp/output.txt')
Path Manipulation
-- Expand special characters
local current_file = vim.fn.expand('%') -- current file name
local full_path = vim.fn.expand('%:p') -- absolute path
local dir = vim.fn.expand('%:p:h') -- directory only
local extension = vim.fn.expand('%:e') -- extension
local basename = vim.fn.expand('%:t') -- filename only
local no_ext = vim.fn.expand('%:t:r') -- filename without extension
-- Resolve path
local resolved = vim.fn.resolve('~/file.txt') -- expand ~, resolve symlinks
-- Get directory of script
local script_dir = vim.fn.expand('<sfile>:p:h')
-- Find executable
if vim.fn.executable('rg') == 1 then
print('ripgrep is available')
end
-- Get standard paths
local config = vim.fn.stdpath('config') -- ~/.config/nvim
local data = vim.fn.stdpath('data') -- ~/.local/share/nvim
local cache = vim.fn.stdpath('cache') -- ~/.cache/nvim
local state = vim.fn.stdpath('state') -- ~/.local/state/nvim
Buffer & Window Info
-- Current buffer
local bufnr = vim.fn.bufnr() -- current buffer number
local bufname = vim.fn.bufname() -- current buffer name
local bufnr_by_name = vim.fn.bufnr('init.lua')
-- Check buffer state
local is_modified = vim.fn.getbufvar(bufnr, '&modified')
local filetype = vim.fn.getbufvar(bufnr, '&filetype')
-- Window info
local winnr = vim.fn.winnr() -- current window number
local winid = vim.fn.win_getid() -- current window ID
local winwidth = vim.fn.winwidth(0) -- current window width
local winheight = vim.fn.winheight(0) -- current window height
Line & Column
-- Line numbers
local current_line = vim.fn.line('.') -- current line (1-based)
local last_line = vim.fn.line('$') -- last line number
local mark_line = vim.fn.line("'a") -- line of mark 'a'
-- Column numbers
local col = vim.fn.col('.') -- current column (1-based)
local end_col = vim.fn.col('$') -- end of line column
-- Virtual column (accounts for tabs)
local vcol = vim.fn.virtcol('.')
-- Get line text
local line_text = vim.fn.getline('.') -- current line
local lines = vim.fn.getline(1, 10) -- lines 1-10
-- Set line text
vim.fn.setline('.', 'New text')
vim.fn.setline(1, { 'Line 1', 'Line 2' })
-- Append lines
vim.fn.append('.', 'Line after cursor')
vim.fn.append(5, { 'Line 6', 'Line 7' })
Search & Patterns
-- Search for pattern
local pos = vim.fn.search('function', 'n') -- don't move cursor
local line = vim.fn.search('function', 'b') -- backward
-- Get search count
local count = vim.fn.searchcount()
print(string.format('%d/%d', count.current, count.total))
-- Match string
local match = vim.fn.match('hello world', 'world') -- returns 6
local matched = vim.fn.matchstr('foo123bar', '\\d\\+')
print(matched) -- "123"
-- Substitute
local result = vim.fn.substitute('hello', 'l', 'L', 'g')
print(result) -- "heLLo"
-- Get last search pattern
local pattern = vim.fn.getreg('/')
String Operations
-- String length
local len = vim.fn.strlen('hello') -- 5
local display_len = vim.fn.strdisplaywidth('hello') -- display width
-- String case
local upper = vim.fn.toupper('hello') -- "HELLO"
local lower = vim.fn.tolower('HELLO') -- "hello"
-- Trim whitespace
local trimmed = vim.fn.trim(' hello ') -- "hello"
-- Split / join
local parts = vim.fn.split('a:b:c', ':') -- { "a", "b", "c" }
local joined = vim.fn.join({ 'a', 'b' }, ':') -- "a:b"
-- Repeat
local repeated = vim.fn['repeat']('x', 5) -- "xxxxx"
Lists & Dictionaries
-- List operations
local list = { 1, 2, 3, 4, 5 }
local reversed = vim.fn.reverse(vim.deepcopy(list)) -- { 5, 4, 3, 2, 1 }
local unique = vim.fn.uniq({ 1, 2, 2, 3 }) -- { 1, 2, 3 }
-- Add to list (modifies in-place in Vimscript, returns copy in Lua)
local extended = vim.fn.extend({ 1, 2 }, { 3, 4 }) -- { 1, 2, 3, 4 }
-- Filter list
local filtered = vim.fn.filter({ 1, 2, 3, 4 }, 'v:val % 2 == 0')
print(vim.inspect(filtered)) -- { 2, 4 }
-- Map list
local mapped = vim.fn.map({ 1, 2, 3 }, 'v:val * 2')
print(vim.inspect(mapped)) -- { 2, 4, 6 }
-- Dictionary operations
local dict = { a = 1, b = 2 }
local keys = vim.fn.keys(dict) -- { "a", "b" }
local values = vim.fn.values(dict) -- { 1, 2 }
local has_key = vim.fn.has_key(dict, 'a') -- 1 (true)
System & Shell
-- Execute shell command
local output = vim.fn.system('ls -la')
print(output)
-- Get command exit code
local exit_code = vim.fn.system('false')
print(vim.v.shell_error) -- 1
-- System with list (no shell interpretation)
local result = vim.fn.system({ 'echo', 'hello' })
-- Get hostname
local hostname = vim.fn.hostname()
-- Get environment variable
local home = vim.fn.getenv('HOME')
local path = vim.fn.getenv('PATH')
-- Set environment (for child processes)
vim.fn.setenv('MY_VAR', 'value')
Time & Date
-- Current time
local timestamp = vim.fn.localtime() -- Unix timestamp
-- Format time
local formatted = vim.fn.strftime('%Y-%m-%d %H:%M:%S', timestamp)
print(formatted) -- "2026-02-20 12:34:56"
-- Relative time (Neovim 0.10+)
local reltime_start = vim.fn.reltime()
-- ... do work ...
local elapsed = vim.fn.reltimefloat(vim.fn.reltime(reltime_start))
print(string.format('Elapsed: %.3f seconds', elapsed))
Registers
-- Get register content
local yanked = vim.fn.getreg('"') -- unnamed register
local search = vim.fn.getreg('/') -- search pattern
local macro_a = vim.fn.getreg('a') -- named register
-- Set register
vim.fn.setreg('a', 'text to store')
vim.fn.setreg('"', { 'line1', 'line2' }) -- multiple lines
-- Get register type
local type = vim.fn.getregtype('a') -- 'v' (char), 'V' (line), '\022' (block)
Marks
-- Get mark position
local mark = vim.fn.getpos("'a")
local line, col = mark[2], mark[3]
-- Set mark
vim.fn.setpos("'a", { 0, 10, 5, 0 }) -- line 10, col 5
-- Get all marks
local marks = vim.fn.getmarklist()
for _, m in ipairs(marks) do
print(m.mark .. ' at line ' .. m.pos[2])
end
-- Get user input
local name = vim.fn.input('Enter name: ')
print('Hello, ' .. name)
-- With default value
local filename = vim.fn.input('Filename: ', 'default.txt')
-- With completion
local file = vim.fn.input('File: ', '', 'file')
local command = vim.fn.input('Command: ', '', 'command')
-- Confirm dialog
local choice = vim.fn.confirm('Save changes?', '&Yes\n&No\n&Cancel', 1)
if choice == 1 then
print('Saving...')
elseif choice == 2 then
print('Discarding...')
else
print('Cancelled')
end
Jobs & Timers
-- Start job
local job_id = vim.fn.jobstart({ 'echo', 'hello' }, {
on_stdout = function(_, data, _)
print(vim.inspect(data))
end,
on_exit = function(_, code, _)
print('Exit code: ' .. code)
end
})
-- Send data to job
vim.fn.chansend(job_id, 'input data\n')
-- Stop job
vim.fn.jobstop(job_id)
-- Wait for job
vim.fn.jobwait({ job_id }, 5000) -- wait up to 5 seconds
-- Timer
local timer = vim.fn.timer_start(1000, function()
print('Timer fired!')
end)
-- Stop timer
vim.fn.timer_stop(timer)
Mode & State
-- Get current mode
local mode = vim.fn.mode()
print(mode) -- 'n', 'i', 'v', etc.
-- Check mode with full info
local mode_full = vim.fn.mode(1) -- returns 'niI' for insert mode with completion
-- Visual selection
local start_line = vim.fn.line('v') -- visual start
local end_line = vim.fn.line('.') -- visual end
local visual_mode = vim.fn.visualmode() -- 'v', 'V', or '\022' (Ctrl-V)
Feature Detection
-- Check if feature is available
if vim.fn.has('nvim-0.9') == 1 then
print('Running Neovim 0.9 or later')
end
if vim.fn.has('python3') == 1 then
print('Python 3 support available')
end
-- Common feature checks
vim.fn.has('clipboard') -- clipboard support
vim.fn.has('timers') -- timer support
vim.fn.has('nvim') -- running in Neovim
vim.fn.has('win32') -- Windows
vim.fn.has('unix') -- Unix-like OS
vim.fn.has('mac') -- macOS
Autoload Functions
Call autoload functions using bracket notation:
-- Call autoload#function()
vim.fn['autoload#function']()
-- Example: plug#begin()
vim.fn['plug#begin']('~/.config/nvim/plugged')
vim.fn['plug#end']()
Special Considerations
Reserved Names
Some function names conflict with Lua keywords. Use bracket notation:
vim.fn['repeat']('x', 5) -- can't use vim.fn.repeat
Most vim.fn functions cannot run in fast callbacks. Use vim.api functions when possible for better performance.
-- Slow: vim.fn in callback
vim.defer_fn(function()
local line = vim.fn.getline('.') -- works but slower
end, 100)
-- Faster: vim.api equivalent
vim.defer_fn(function()
local line = vim.api.nvim_get_current_line() -- faster
end, 100)
Return Value Handling
-- v:null becomes vim.NIL
local result = vim.fn.get({ a = 1 }, 'b', vim.NIL)
if result == vim.NIL then
print('Key not found')
end
-- Empty dict vs empty list
local empty_list = {} -- becomes List in Vimscript
local empty_dict = vim.empty_dict() -- becomes Dict in Vimscript
Calling from Vimscript
You can call Lua functions from Vimscript using luaeval():
" Call Lua from Vimscript
let result = luaeval('vim.fn.expand("%:p")')
Comparison with vim.api
| Feature | vim.fn | vim.api |
|---|
| Source | Vimscript functions | Native API |
| Performance | Slower (marshalling overhead) | Faster |
| Type Conversion | Automatic | Minimal |
| Coverage | All Vimscript functions | Core operations |
| Fast Callbacks | Not allowed | Some allowed |
| Preferred For | Legacy code, complex Vimscript logic | New code, performance-critical |
Examples
File Explorer
function show_files()
local files = vim.fn.glob('*', false, true)
for _, file in ipairs(files) do
local type = vim.fn.isdirectory(file) == 1 and 'DIR' or 'FILE'
print(string.format('[%s] %s', type, file))
end
end
show_files()
Quick Grep
function grep_current_dir(pattern)
local matches = vim.fn.systemlist({ 'rg', '--vimgrep', pattern })
vim.fn.setqflist({}, 'r', {
title = 'Grep: ' .. pattern,
lines = matches
})
vim.cmd('copen')
end
grep_current_dir('TODO')
Buffer Switcher
function switch_to_buffer(name_part)
local buffers = vim.fn.getbufinfo({ buflisted = 1 })
for _, buf in ipairs(buffers) do
if buf.name:match(name_part) then
vim.api.nvim_set_current_buf(buf.bufnr)
return
end
end
print('Buffer not found: ' .. name_part)
end
switch_to_buffer('init.lua')
See Also
- vim (Core Module) - Core vim functions
- vim.api - Native API functions
:help vim-function - Vimscript function reference
:help function-list - Complete function list