Beyond Keymaps: Functions as First‑Class Tools
Most Neovim users start by remapping keys. That’s fine, but it stops short of turning the editor into a programmable environment. The real power comes from custom Lua functions that do things no plugin can anticipate: running code asynchronously, replacing visual selections with computed results, exploring your command history like a file, or inspecting registers at a glance.
In this article we open lua/config/functions.lua – the heart of the automation
layer – and walk through every major utility. You’ll see the full source for each function,
understand why it was built, and learn how to wire it into your own workflow.
Architecture Overview
The file lua/config/functions.lua exports a table of functions. These are
called from keymaps (defined in keymaps.lua) and user commands (defined in
commands.lua). The module is loaded once during startup and reused everywhere.
The directory structure relevant to this article:
lua/config/
├── functions.lua <-- all custom logic
├── keymaps.lua <-- maps keys to functions
└── commands.lua <-- creates :UserCommands
1. Async Python and LaTeX Runners
Two of the most frequently used functions are run_python_selection() and
run_python_and_replace(). The first executes the visually selected Python
code in a non‑blocking job, appending its output to a scratch buffer. The second does the
same, but replaces the selection with the output – ideal for quick calculations
directly in your code or prose.
Both functions use vim.fn.jobstart to avoid freezing the UI, and they
automatically handle buffer cleanup when the job finishes or the user presses Enter.
A similar async pattern is used in the LaTeX ftplugin (covered in Article #2), but
the core helper functions are general enough to be reused for any language.
function M.run_python_selection()
-- Check if in visual mode
local mode = vim.fn.mode()
if mode ~= 'v' and mode ~= 'V' then
print("No visual selection")
return
end
-- Save current register
local save_reg = vim.fn.getreg('"')
local save_regtype = vim.fn.getregtype('"')
-- Yank selection into default register
vim.cmd('normal! gvy')
local code = vim.fn.getreg('"')
-- Restore original register
vim.fn.setreg('"', save_reg, save_regtype)
-- Trim whitespace and prepend math import
code = code:gsub('^%s*', ''):gsub('%s*$', '')
code = "from math import *\n" .. code
-- Execute Python code safely
local ok, output = pcall(vim.fn.system, {'python3', '-c', code})
if not ok then
print("Error executing Python code")
return
end
-- Open scratch buffer
vim.cmd('enew')
vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.split(output, "\n"))
-- Configure buffer
vim.bo.buftype = 'nofile'
vim.bo.bufhidden = 'wipe'
vim.bo.swapfile = false
vim.wo.wrap = false
end
The run_python_and_replace() variant handles single‑line selections specially:
it wraps the expression in a print() so that the output replaces the expression
inline. This is useful for evaluating math expressions inside Markdown or LaTeX documents.
function M.run_python_and_replace()
local save_reg = vim.fn.getreg('"')
local save_regtype = vim.fn.getregtype('"')
vim.cmd('normal! ""gvy')
local code = vim.fn.getreg('"')
vim.fn.setreg('"', save_reg, save_regtype)
code = code:gsub('^%s*', ''):gsub('%s*$', '')
local start_line = vim.fn.line("'<")
local end_line = vim.fn.line("'>")
if start_line == end_line then
code = 'print(' .. code .. ')'
end
code = "from math import *\n" .. code
local output = vim.fn.system('python3 -c ' .. vim.fn.shellescape(code))
output = output:gsub('\n+$', '')
vim.cmd('normal! gv"_c' .. output)
end
2. Command History Explorer
The command‑line history in Vim is powerful but invisible. My command_history_explorer()
function dumps the entire history (newest first) into a temporary buffer. You can navigate it
with j/k or arrow keys, and pressing Enter executes the command under
the cursor. Pressing q or Tab closes the buffer.
This effectively gives you a “command palette” with zero dependencies. Because the buffer
is non‑modifiable and immediately searchable with /, you can filter through
hundreds of past commands in seconds.
function M.command_history_explorer()
local history = {}
local count = vim.fn.histnr(':') -- number of commands in history
-- Collect commands from newest to oldest
for i = count, 1, -1 do
local cmd = vim.fn.histget(':', i)
if cmd ~= "" then
table.insert(history, cmd)
end
end
-- Create new buffer
vim.cmd('enew')
local buf = vim.api.nvim_get_current_buf()
vim.api.nvim_buf_set_lines(buf, 0, -1, false, history)
-- Buffer options
vim.bo[buf].buftype = 'nofile'
vim.bo[buf].bufhidden = 'wipe'
vim.bo[buf].swapfile = false
vim.bo[buf].modifiable = false
vim.wo.wrap = false
local opts = { buffer = buf, noremap = true, silent = true }
-- Map arrow keys (raw sequences)
vim.keymap.set('n', "\27[B", 'j', opts) -- Down
vim.keymap.set('n', "\27[A", 'k', opts) -- Up
-- Also allow hjkl
vim.keymap.set('n', 'j', 'j', opts)
vim.keymap.set('n', 'k', 'k', opts)
-- Quit buffer quickly
vim.keymap.set('n', 'q', function() vim.cmd('bd! ' .. buf) end, opts)
-- Execute the command under cursor
vim.keymap.set('n', '', function()
local cmd = vim.fn.getline('.')
vim.cmd('bd! ' .. buf) -- close history buffer
vim.cmd(cmd) -- execute command
end, opts)
-- Start at the top
vim.cmd('normal! gg')
end
A variant, search_command_history(), opens a modifiable buffer so that
incsearch can highlight matches. This is useful when you want to interactively
search without closing the buffer.
3. Register Inspector
Registers are one of Vim’s most powerful features, yet most users never know what’s stored
in them. The output_regs() function prints the contents of registers
a through z into a scratch buffer, labelled clearly. It’s a
simple idea, but it turns registers into a visible, reviewable tool.
function M.output_regs()
vim.cmd('enew')
vim.bo.buftype = 'nofile'
vim.bo.bufhidden = 'wipe'
vim.bo.swapfile = false
vim.wo.wrap = false
local content = {}
for reg = string.byte('a'), string.byte('z') do
local reg_char = string.char(reg)
local reg_content = vim.fn.getreg(reg_char)
if reg_content ~= '' then
table.insert(content, reg_char .. ":")
table.insert(content, reg_content)
table.insert(content, "")
end
end
vim.api.nvim_buf_set_lines(0, 0, -1, false, content)
vim.cmd('normal! gg')
print("Contents of registers a to z")
end
4. Select Inside Any Pair
Vi’s i{, i(, etc. are great, but they require you to think
about which delimiter you’re inside. select_inside_any_pair() tries every
common pair ({}, [], (), $...$,
quotes) and selects the innermost one that surrounds the cursor. If nothing matches, it
falls back to selecting the current line.
function M.select_inside_any_pair()
local savepos = vim.fn.getpos('.')
local pairs = {
{'{', '}'},
{'[', ']'},
{'(', ')'},
{'$', '$'},
{'"', '"'},
{"'", "'"}
}
for _, pair in ipairs(pairs) do
local open, close = pair[1], pair[2]
local start, finish
if open == close then
start = vim.fn.searchpos('\\V' .. open, 'bnW')
finish = vim.fn.searchpos('\\V' .. close, 'nW')
else
start = vim.fn.searchpairpos('\\V' .. open, '', '\\V' .. close, 'bnW')
finish = vim.fn.searchpairpos('\\V' .. open, '', '\\V' .. close, 'nW')
end
if start[1] > 0 and finish[1] > 0 then
local cur = vim.fn.getpos('.')
local start_before_cursor = (start[1] < cur[2]) or (start[1] == cur[2] and start[2] <= cur[3])
local finish_after_cursor = (finish[1] > cur[2]) or (finish[1] == cur[2] and finish[2] >= cur[3])
if start_before_cursor and finish_after_cursor then
vim.fn.setpos('.', {0, start[1], start[2] + 1, 0})
vim.cmd('normal! v')
vim.fn.setpos('.', {0, finish[1], finish[2], 0})
vim.cmd('normal! h')
return
end
end
end
-- Fallback: select current line
local lnum = vim.fn.line('.')
local line = vim.fn.getline(lnum)
local end_col = #line
if end_col > 0 and line:sub(-1) == '$' then
end_col = end_col - 1
end
vim.fn.setpos('.', {0, lnum, 1, 0})
vim.cmd('normal! v')
vim.fn.setpos('.', {0, lnum, end_col, 0})
end
5. Old Files Browser and Ranger Chooser
Vim stores recently opened files in vim.v.oldfiles. output_old_files()
dumps them into a buffer, making each entry a link you can press Enter on to open. This is
a lightweight alternative to Telescope for quickly revisiting files without fuzzy matching.
The ranger_chooser() function integrates the ranger file manager
directly into the editor: it runs ranger --choosefiles in a terminal (or XTerm
if a GUI is detected) and opens all selected files as arguments. This fits the philosophy
of using external, best‑in‑class tools while keeping the editor as the central hub.
function M.output_old_files()
vim.cmd('enew')
vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.v.oldfiles)
vim.bo.buftype = 'nofile'
vim.bo.bufhidden = 'wipe'
vim.bo.swapfile = false
vim.wo.wrap = false
vim.keymap.set('n', '', function()
local line = vim.fn.getline('.')
vim.cmd('e ' .. line)
end, { buffer = true })
local buf = vim.api.nvim_get_current_buf()
vim.keymap.set('n', 'q', function() vim.cmd('bd! ' .. buf) end, { buffer = buf, noremap = true, silent = true })
vim.keymap.set('n', '', function() vim.cmd('bd! ' .. buf) end, { buffer = buf, noremap = true, silent = true })
vim.cmd('normal! gg')
end
6. Word Count, URL Maker, and Directory Prefix
Several smaller utilities fill in the gaps:
word_count()– runsdetexon a LaTeX file and pipes it throughwc -wto count plain‑text words (ignoring markup).make_url(word)– replaces every occurrence of a word with an HTML link pointing tohttps://word.com. Useful for quickly linking to domains in blog articles.get_dir_number()– extracts the leading numeric prefix of the current working directory, used in LaTeX snippet auto‑numbering.
7. Save Bindings and Functions
save_vim_bindings_and_functions() captures all current key mappings and
function definitions into a buffer. This is invaluable for debugging or documenting
your setup. It uses Vim’s :redir mechanism to capture the output of
:map and :function and writes them into a scratch buffer.
Integration: Keymaps and Commands
These functions are exposed through two files:
-- keymaps.lua (excerpt)
vim.keymap.set("v", "c", functions.run_python_selection, { noremap = true })
vim.keymap.set("v", "r", functions.run_python_and_replace, { noremap = true })
vim.keymap.set("n", "wc", functions.word_count, opts)
vim.keymap.set("n", "mm", functions.output_old_files, opts)
vim.keymap.set("n", "cc", functions.command_history_explorer, opts)
vim.keymap.set("n", "rr", functions.output_regs, opts)
vim.keymap.set("n", "ranger", functions.ranger_chooser, opts)
-- commands.lua (excerpt)
vim.api.nvim_create_user_command("WC", functions.word_count, {})
vim.api.nvim_create_user_command("Mm", functions.output_old_files, {})
vim.api.nvim_create_user_command("Cc", functions.command_history_explorer, {})
vim.api.nvim_create_user_command("Ss", functions.search_command_history, {})
vim.api.nvim_create_user_command("Rr", functions.output_regs, {})
vim.api.nvim_create_user_command("HH", functions.wrap_code_in_buffer, {})
vim.api.nvim_create_user_command("RangerChooser", functions.ranger_chooser, {})
vim.api.nvim_create_user_command("EngType", functions.eng_type, {})
vim.api.nvim_create_user_command("FrType", functions.fr_type, {})
User commands (e.g., :WC, :Cc) are intentionally uppercase
and short to minimise typing. Keymaps are mnemonic: <leader>cc for
command history, <leader>rr for
registers, etc.
Design Philosophy: Functions as Infrastructure
The unifying principle behind these utilities is that they treat Neovim as a programmable platform, not just a text editor. Each function solves a concrete, recurring need in the developer’s daily workflow:
- Async execution – never block the UI.
- Scratch buffers – temporary, non‑file buffers that clean up after themselves.
- Explicit buffer management – every function that opens a buffer also defines
how to close it (
qorbd!). - Pure Lua, no plugin dependencies – everything runs on the built‑in API, making the config portable.
The result is an editor that behaves like an IDE, but without the weight. In the next article, we’ll see how snippets push this automation even further, turning tiny triggers into large, context‑aware code blocks.