Turning Functions Into a Control Surface
Custom functions (covered in Article #3) are the engine; keymaps and user commands are the steering wheel and dashboard. A well‑designed Neovim configuration makes the most frequent operations available without a thought – and the less frequent ones discoverable through muscle‑memory shortcuts or short colon commands.
This article opens lua/config/keymaps.lua and lua/config/commands.lua
and shows how they form a unified toolbelt. Every mapping is intentionally chosen; every
command mirrors a function but adds a discoverability layer. We’ll also examine the ranger
file‑chooser integration and the buffer‑wrapping utilities that bridge the editor to external
workflows.
The Keymaps Layer
The file lua/config/keymaps.lua is loaded in the scheduled callback of
init.lua, after plugins and functions are available. It defines global
keybindings that never depend on a specific filetype – filetype‑specific mappings live
in ftplugin/ (Article #2). This separation keeps the global mapset clean
and predictable.
The complete keymaps file:
local opts = { noremap = true, silent = true }
local functions = require("config.functions")
-- Copy whole file to system clipboard
vim.keymap.set("n", "cc", 'gg"+yG', opts)
vim.keymap.set("v", "", '"+y', opts)
vim.keymap.set("v", "7", '"+y', opts)
vim.keymap.set("i", "", '+', opts)
-- Toggle line numbers
vim.keymap.set("n", "l", ":set number! relativenumber!", opts)
-- redo
vim.keymap.set("n", "r", '', opts)
-- Buffer navigation
vim.keymap.set("n", "", ":bn", opts)
vim.keymap.set("n", "", ":bp", opts)
-- Window movements
vim.keymap.set("n", "", "", opts)
vim.keymap.set("n", "", "", opts)
-- Python selection execution (Visual mode)
vim.keymap.set("v", "c", functions.run_python_selection, { noremap = true })
vim.keymap.set("v", "r", functions.run_python_and_replace, { noremap = true })
-- Run scripts on files/selection
vim.keymap.set("n", "rf", functions.run_scripts_on_files, opts)
vim.keymap.set("v", "rs", functions.run_scripts_on_select, { noremap = true })
-- Basic file commands
vim.keymap.set("n", "w", ":w", opts)
vim.keymap.set("n", "q", ":q", opts)
vim.keymap.set("n", "h", ":nohlsearch", opts)
-- Window navigation using Ctrl + hjkl
vim.keymap.set("n", "", "h", opts)
vim.keymap.set("n", "", "j", opts)
vim.keymap.set("n", "", "k", opts)
vim.keymap.set("n", "", "l", opts)
-- Selection and utility commands
vim.keymap.set("n", "v", functions.select_inside_any_pair, opts)
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", "hh", functions.wrap_code_in_buffer, opts)
vim.keymap.set("n", "ranger", functions.ranger_chooser, opts)
vim.keymap.set("n", "ì", "^", opts)
vim.keymap.set("o", "ì", "^", opts)
local ls = require("luasnip")
-- Forward jump to <++> (now using Ctrl+L)
vim.cmd([[
inoremap /<++>"_c4l
nnoremap /<++>"_c4l
]])
-- Backward jump to <++> (now using Ctrl+H)
vim.cmd([[
inoremap ?<++>"_c4l
nnoremap ?<++>"_c4l
]])
-- LuaSnip forward/backward jumps with Ctrl+J / Ctrl+K
vim.keymap.set("i", "", function() require("luasnip").jump(1) end, { silent = true })
vim.keymap.set("i", "", function() require("luasnip").jump(-1) end, { silent = true })
Several design patterns stand out:
- Mnemonic leader prefixes:
<leader>wcfor word count,<leader>ccfor command history,<leader>rrfor registers. The leader key is,so these are fast two‑stroke combos. - Buffer and window movement:
Tab/Shift‑Tabfor buffer switching,Ctrl‑hjklfor window navigation. No arrow‑key dependency, minimal finger travel. - Placeholder jumping:
Ctrl‑LandCtrl‑Hjump to the next/previous<++>placeholder – a convention used throughout snippets and insert‑mode expansions. This is separate from LuaSnip’sCtrl‑J/Ctrl‑Kfor snippet node jumps. - Clipboard shortcuts:
ccin normal mode copies the entire file.Ctrl‑cin visual mode copies selection.Ctrl‑vin insert mode pastes.
User Commands: The Discoverable Layer
Keymaps are fast but invisible. User commands (the : interface) serve as the
discoverable, self‑documenting counterpart. The file lua/config/commands.lua
creates one command per major function:
local functions = require("config.functions")
-- Create user commands
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("MM", functions.output_old_files, {})
vim.api.nvim_create_user_command("Cc", functions.command_history_explorer, {})
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("SS", functions.search_command_history, {})
vim.api.nvim_create_user_command("Rr", functions.output_regs, {})
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("SaveVimInfo", functions.save_vim_bindings_and_functions, {})
vim.api.nvim_create_user_command("EngType", functions.eng_type, {})
vim.api.nvim_create_user_command("FrType", functions.fr_type, {})
Commands are kept short (often two or three uppercase letters) to minimise typing. A few
have dual‑case variants (e.g., :Mm and :MM) – the uppercase version
is a deliberate redundancy, ensuring the command still works if Caps Lock is on.
Every command mirrors a function from functions.lua but requires no modifier
key. This makes the toolbelt accessible from both muscle‑memory (keymap) and conscious
recall (command palette / command history).
The Ranger Chooser: External Tool Integration
The ranger_chooser() function (and its :RangerChooser command)
launches the ranger terminal file manager and opens all selected files as
Neovim arguments. It detects whether Neovim is running in a GUI or terminal and adapts:
function M.ranger_chooser()
local temp = vim.fn.tempname()
if vim.fn.has('gui_running') == 1 then
vim.fn.system('xterm -e ranger --choosefiles=' .. vim.fn.shellescape(temp))
else
vim.fn.system('ranger --choosefiles=' .. vim.fn.shellescape(temp))
end
if vim.fn.filereadable(temp) == 0 then
vim.cmd('redraw!')
return
end
local names = vim.fn.readfile(temp)
if #names == 0 then
vim.cmd('redraw!')
return
end
vim.cmd('edit ' .. vim.fn.fnameescape(names[1]))
for i = 2, #names do
vim.cmd('argadd ' .. vim.fn.fnameescape(names[i]))
end
vim.cmd('redraw!')
end
This is the Unix philosophy in action: let ranger do what it does best (file
navigation) and let Neovim do what it does best (editing). The integration is a thin glue
layer – a temporary file is used to pass the selected paths back, and Neovim’s argument
list (:args) is populated so you can navigate between the chosen files with
:next and :prev.
The HH Wrapper: Format Conversion on Demand
The wrap_code_in_buffer() function (bound to <leader>hh and
:HH) takes the current buffer’s content and wraps it in
<pre><code> tags, opening the result in a new buffer with HTML
filetype. This is used constantly when writing articles that embed code blocks.
function M.wrap_code_in_buffer()
local save_cursor = vim.fn.getpos('.')
local buffer_text = vim.api.nvim_buf_get_lines(0, 0, -1, false)
vim.cmd('enew')
vim.bo.buftype = 'nofile'
vim.bo.bufhidden = 'wipe'
vim.bo.swapfile = false
vim.wo.wrap = false
vim.bo.filetype = 'html'
local content = '\n' ..
table.concat(buffer_text, "\n") ..
'\n
'
vim.api.nvim_buf_set_lines(0, 0, -1, false, vim.split(content, "\n"))
vim.fn.setpos('.', save_cursor)
end
A similar wrapper exists in the LaTeX ftplugin (Article #2), but this global version works on any buffer. Together they form a small but powerful content pipeline: write in your preferred language, hit a key, get a properly escaped HTML code block ready for the blog.
Composability and Consistency
The keymaps and commands layer follows a strict rule: every keybinding has a corresponding user command, and every command delegates to a single function. There are no inline Vimscript hacks, no copy‑pasted logic. This makes the system:
- Auditable: you can trace any action from keypress to function in seconds.
- Remappable: changing a keybinding never breaks the command‑line interface, and vice versa.
- Documentable: the user commands themselves serve as living documentation.
The toolbelt doesn’t try to do everything – it focuses on operations that are too frequent to type out manually but too specific for a general‑purpose plugin. Combined with the filetype‑specific maps (Article #2) and the snippet system (Article #4), the editor becomes a personalised engineering console.
In the final article, we’ll survey the plugin ecosystem – Treesitter, completion, LSP, and bufferline – and how they complete the developer dashboard.