last updated: July 28, 2025
7 minute read
Five Interesting Things From my Neovim Config (2025 Edition)
Back in 2022, I wrote a post titled Five Interesting Things From my Neovim Config with a handful of tips I came up with when moving from VSCode to Neovim. It's been a few years since, my config has changed quite a bit, and I like to think my vim knowledge has grown. So - it's time for a sequel!
1. Require-ing every file in a directory
One convenient aspect of the lazy.nvim
plugin manager is its approach to
structuring plugin configuration files: any
~/.config/nvim/lua/plugins/*.lua
file is automatically sourced and its configuration applied - no
manual require
necessary.
I personally don't use lazy.nvim
(I prefer the simplicity of
paq-nvim
), but I really like the idea of automatically
require
-ing every lua module in a directory. I prefer to split up plugin configuration into one
file per "area", and it feels a little clunky to manually require each file. After tinkering for a
bit, I was able to write a simple function to handle this for me:
--- @param dir string i.e. "plugins"local require_dir = function(dir)-- ~/.config/nvim/lua/local base_lua_path = vim.fs.joinpath(vim.fn.stdpath "config", "lua")-- i.e. ~/.config/nvim/lua/plugins/*.lualocal glob_path = vim.fs.joinpath(base_lua_path, dir, "*.lua")local paths_str = vim.fn.glob(glob_path)local paths_tbl = vim.split(paths_str, "\n")for _, path in pairs(paths_tbl) do-- convert absolute filename to relative-- ~/.config/nvim/lua/plugins/config_file.lua -> plugins/config_filelocal relfilename = vim.fs.relpath(base_lua_path, path):gsub(".lua", "")require(relfilename)endend
Just one call to h.require_dir "plugins"
.
2. A guide to choosing remaps
This second point is less of a tip and more of a philosophy I've developed when creating remaps. Philosophy is a bit of a strong word, but I find it useful nonetheless.
-
If there's a builtin remap, use it
There was a time when I remapped:
J
to:cnext
K
to:cprev
Z
toJ
gh
tovim.lsp.buf.hover
(the defaultK
remap).
I wanted
J
andK
to navigate the quickfix list, and I was willing to sacrifice any and all native remap to get to that goal.But Neovim already has native remaps for
:cnext
and:cprev
:]q
and[q
! By using the]
remaps instead, I was able to restore the nativeJ
K
remaps, and everything felt simpler. -
Extend builtin remaps
If a remap makes sense as an extension of an existing, builtin remap, I try to use it. For example, I use the native
gri
/gra
/grn
/grr
LSP remaps. However, one LSP function without a builtin remap isvim.lsp.buf.type_definition
- which I use quite frequently. Following the pattern of thegr*
remaps, something likegry
works great. -
Prioritize one-letter remaps
Earlier versions of my config had too many
<leader>
-two-letter remaps and too few<leader>
-one-letter remaps. At the time, I was nervous about hogging a letter for a one-letter remap when I could use that letter to begin 26 different two-letter remaps instead. How many remaps did I think I would write?One-letter remaps are valuable real estate, and they should be used! These days, I try to use as many one-letter remaps as I can with as few two-letter remaps as possible. In my config today, 21 letters are used as one-letter remaps, with only 5 for two letter remaps.
Few of these one-letter remaps have mnemonic meaning, but that's a tradeoff I'm happy to make.
3. Barebones snippets with the read command
There's a great vim talk titled How to Do 90% of What Plugins Do (With Just Vim) that I've watched a handful of times - it's really fun, I'd recommend it.
One of the tips in the talk is to use the :read
command to act as a barebones snippet replacement.
The idea is to split your snippets into one snippet per file and use the :read
command to insert
the content of the snippet file at your cursor. By default, :read
will insert a file below the
cursor - but prepending the command with -1
inserts the file at the cursor instead. Optionally,
you can also execute vim movements with :normal!
to move the cursor to the correct spot before
editing.
There's a few reasons I really like this approach:
- It's simple, no plugin required
- No snippet syntax to learn, just the plain text of your snippet along with some vim movements
- Snippets can be stored in files that match their language, allowing syntax highlighting to function properly
Here's the code I use:
vim.keymap.set("i", "<C-t>", "<C-o>:Snippet<space>")vim.keymap.set({ "n", "v", }, "<C-t>", function()vim.notify("Snippets are only supported in insert mode", vim.log.levels.ERROR)end)vim.api.nvim_create_user_command("Snippet", function(opts)local snippet_trigger_to_file_mapping = {eff = { file = "useEffect.ts", movement = "ji\t", },-- ...}local snippet_trigger = opts.fargs[1]if snippet_trigger == nil thenprint "Available snippet triggers:"for trigger in pairs(snippet_trigger_to_file_mapping) doprint(trigger)endreturnendlocal snippet_triggers = vim.tbl_keys(snippet_trigger_to_file_mapping)if not vim.tbl_contains(snippet_triggers, snippet_trigger) thenvim.notify(snippet_trigger .. " is not a valid snippet trigger", vim.log.levels.ERROR)returnend-- I store my snippets at ~/.config/nvim/snippets/local snippets_path = vim.fs.joinpath(vim.fn.stdpath "config", "snippets")local snippet_file = vim.fs.joinpath(snippets_path, snippet_trigger_to_file_mapping[snippet_trigger].file)vim.cmd("-1read " .. snippet_file)vim.cmd("normal! " .. snippet_trigger_to_file_mapping[snippet_trigger].movement)end, { nargs = "*", })
Using a barebones approach like this lacks some of the fancier features of "true" snippets - but it does everything I need it to, and that's enough for me.
4. Splitting up remaps by concatenating
Complex vim remaps can be a lot of fun to write, but a pain to read. Recently, I started splitting up complex remaps into variables and concatenating them into a big string. It's verbose, but in my opinion it's simpler to maintain.
For example, I use the following remaps to copy the current line / visual selection, comment it, and paste it un-commented below.
-- `expr = true` is necessary when returning a string that's meant to be run-- `remap = true` is necessary to have the native `gcc` comment as expectedvim.keymap.set("n", "<leader>yc",function()local z_register = [["z]]local yank = "yy"local comment = "gcc"local paste = "p"return z_register .. yank .. comment .. z_register .. pasteend,{ expr = true, remap = true, desc = "Yank the current line, comment it, and paste it below", })vim.keymap.set("v", "<leader>yc",function()local z_register = [["z]]local yank_and_unselect = "y"local move_to_end_selection = "`>"local paste = "p"local reselect_last = "gv"local comment_selection = "gc"returnz_register ..yank_and_unselect ..reselect_last ..comment_selection ..move_to_end_selection ..z_register ..paste ..move_to_end_selectionend, { expr = true, remap = true, desc = "Yank the current selection, comment it, and paste it below", })
5. The Poor Man's Harpoon
After reading this github discussion on replacing Harpoon with global marks, I was inspired to try my own hand at a similar set of remaps.
The idea behind these remaps is to bookmark "active" files with global marks and view them with a picker - simple, but an effective way to keep track of a handful of files at once.
In my version, I wanted to set a global mark without ever thinking about the name of the mark
itself - it doesn't really matter to me if the mark is A
or B
, I just want the file bookmarked.
--- @param mark_name stringlocal function is_global_mark_unset(mark_name)local mark = vim.api.nvim_get_mark(mark_name, {})-- :h nvim_get_mark-- (0, 0, 0, '') is returned if the mark is not setreturn mark[1] == 0 and mark[2] == 0 and mark[3] == 0 and mark[4] == ""end--- @param mark_name stringlocal function is_buffer_mark_unset(mark_name)local mark = vim.api.nvim_buf_get_mark(0, mark_name)-- :h nvim_buf_get_mark-- (0, 0) is returned if the mark is not setreturn mark[1] == 0 and mark[2] == 0endlocal global_marks = ("abcdefghijklmnopqrstuvwxyz"):upper()vim.keymap.set("n", "gm", function()--- @param letter stringlocal function set_mark(letter)local row = vim.fn.line "."local col = 0vim.api.nvim_buf_set_mark(0, letter, row, col, {})vim.notify(("Set global mark %s to line %s"):format(letter, row))end-- If a global mark exists in the current buffer, update it to the current linefor letter in global_marks:gmatch "." dolocal is_buffer_mark_set = not is_buffer_mark_unset(letter)if is_buffer_mark_set thenset_mark(letter)returnendendlocal next_avail_mark = nilfor letter in global_marks:gmatch "." doif is_global_mark_unset(letter) thennext_avail_mark = letterbreakendendif next_avail_mark == nil thenvim.notify "No global marks available"returnend-- Add a global mark to the bufferset_mark(next_avail_mark)end, { desc = "Set a global mark for the buffer", })vim.keymap.set("n", "dgm", function()for letter in global_marks:gmatch "." dolocal is_buffer_mark_set = not is_buffer_mark_unset(letter)if is_buffer_mark_set thenvim.api.nvim_del_mark(letter)vim.notify("Deleted global mark " .. letter)returnendendvim.notify "No global mark in the buffer"end, { desc = "Delete a global mark for the buffer", })
you might also like:
Five Interesting Things From my Neovim Config (2022 Edition)
October 16, 2022
A few interesting tips I've picked up from tinkering with my config