Vim's vertical navigation commands can be limiting, and users often rely
on methods like manual line counting (5j
) to reach their desired
location. It would be useful to have a way to navigate based on the
semantic structure of a buffer, with meaningful points of reference.
Fortunately, this problem is already solved in another area: code folding. Neovim's Treesitter support provides a high-quality repository of queries that define folds based on programming language semantics, which can also power vertical navigation.
Vim provides some built in commands for navigating folds: [z, z], zj and zk. However, these have some shortcomings:
- The
zk
keybinding moves to the end of the previous fold rather than the most recent start of a fold, which is unintuitive - The cursor is moved to the very start of the line (unlike other vertical navigation commands)
- The keybindings are awkward which makes it difficult to repeat motions or compose multiple motions together
- There is no visual feedback of the fold structure while navigating
This plugins fixes these shortcomings.
demo.mp4
The demo was recorded with the lazy.nvim example config shown below.
Foldnav requires a buffer with folds to operate. To use treesitter for folding, add the following Lua code to your config:
vim.opt.foldmethod = "expr"
vim.opt.foldexpr = "v:lua.vim.treesitter.foldexpr()"
vim.opt.foldlevelstart = 99 -- load buffers with folds open
See treesitter-parsers to enable treesitter for more filetypes.
To test if folding works, run :set foldcolumn=auto:9
. This shows all
the folds for your current file in the left margin (see video above).
Example using lazy.nvim to install, enable highlighting, and map the Ctrl modifier:
{
"domharries/foldnav.nvim",
version = "*",
config = function()
vim.g.foldnav = {
flash = {
enabled = true,
},
}
end,
keys = {
{ "<C-h>", function() require("foldnav").goto_start() end },
{ "<C-j>", function() require("foldnav").goto_next() end },
{ "<C-k>", function() require("foldnav").goto_prev_start() end },
-- { "<C-k>", function() require("foldnav").goto_prev_end() end },
{ "<C-l>", function() require("foldnav").goto_end() end },
},
},
Example using vim.keymap.set
to map the Alt modifier:
vim.keymap.set("n", "<M-h>", function() require("foldnav").goto_start() end)
vim.keymap.set("n", "<M-j>", function() require("foldnav").goto_next() end)
vim.keymap.set("n", "<M-k>", function() require("foldnav").goto_prev_start() end)
-- vim.keymap.set("n", "<M-k>", function() require("foldnav").goto_prev_end() end)
vim.keymap.set("n", "<M-l>", function() require("foldnav").goto_end() end)
These mappings are defined for normal mode, but you could also define
them for visual and operator pending mode by changing the first argument
to {"n", "x", "o"}
. See
:map-modes
for more information.
The movements that this plugin provides are shown below, with their equivalent vim commands:
Function | vim | Target |
---|---|---|
goto_start() |
[z |
Start of the enclosing fold |
goto_next() |
zj |
Start of next fold |
goto_prev_start() |
None | The most recent place that a fold started |
goto_prev_end() |
zk |
End of the previous fold |
goto_end() |
]z |
End of the enclosing fold |
Fold navigation maps quite nicely onto the standard vim hjkl
movement
keys, but there are two options for mapping Mod+k.
Pros for goto_prev_start()
:
- More intuitive for most people
- Mod+k always does the reverse of Mod+j
Pros for goto_prev_end()
:
- Matches the built in vim movements - it's easy to map a few keys on vanilla vim to get functionality that is mostly equivalent. See Alternatives below.
- Can use Mod+ljljlj and Mod+khkhkh to go up and down at a constant level of nesting (useful for JSON)
- To reverse a Mod+j navigation, you can use Ctrl+o to navigate backwards in the jumplist.
Of course it is perfectly possible to map both functions with different keys.
To configure the plugin to go to the start or end of the line when
navigating, you can call ^
or $
at the end of the mapping, e.g.
vim.keymap.set("n", "<C-h>", function()
require("foldnav").goto_start()
vim.cmd.normal("^")
end)
The bindings shown so far will navigate across multiple fold levels. The following mappings will navigate to the next and previous fold on the same level where possible:
vim.keymap.set("n", "<M-n>", function()
local foldnav = require("foldnav")
foldnav.goto_end()
foldnav.goto_next()
end)
vim.keymap.set("n", "<M-p>", function()
local foldnav = require("foldnav")
foldnav.goto_prev_end()
foldnav.goto_start()
end)
Foldnav is configured with a global variable vim.g.foldnav
. There is
no need to set this variable if you want to use the defaults. These are
all the settings at their default values:
vim.g.foldnav = {
flash = {
enabled = false,
mode = "fold", -- or "opposite"
duration_ms = 300
}
}
Parameter | Default | Description |
---|---|---|
flash.enabled | false |
Enable highlighting fold after navigation |
flash.mode | "fold" |
"fold" = entire fold, "opposite" = other edge of fold |
flash.duration_ms | 300 |
Highlight duration in milliseconds |
Individual parameters can be changed for the current session by using
:let
on the command line:
:let g:foldnav.flash.enabled = v:true
:let g:foldnav.flash.mode = "opposite"
Note: directly setting individual values does not work in Lua, see lua-vim-variables.
The highlight group used for the flash is FoldnavFlash
. By default it
links to the CursorLine
highlight group but can be customised using
:highlight
or nvim_set_hl().
Excluding the goto_prev_start()
function, most of what this plugin
does can be approximated with the following Lua code (Ctrl
modifier shown):
vim.keymap.set("", "<C-h>", "[zjk")
vim.keymap.set("", "<C-j>", "zjkj")
vim.keymap.set("", "<C-k>", "zkjk")
vim.keymap.set("", "<C-l>", "]zkj")
Or the following vimscript:
noremap <C-h> [zjk
noremap <C-j> zjkj
noremap <C-k> zkjk
noremap <C-l> ]zkj
The jk
and kj
suffixes put the cursor in the correct column after
navigation.