Skip to main content
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 TypeLua Type
Numbernumber
Floatnumber
Stringstring
Listtable (array)
Dicttable (key-value)
v:true / v:falsetrue / false
v:nullvim.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

Input / Prompt

-- 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

Performance

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

Featurevim.fnvim.api
SourceVimscript functionsNative API
PerformanceSlower (marshalling overhead)Faster
Type ConversionAutomaticMinimal
CoverageAll Vimscript functionsCore operations
Fast CallbacksNot allowedSome allowed
Preferred ForLegacy code, complex Vimscript logicNew 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

Build docs developers (and LLMs) love