Navigating Markdown Headings with Treesitter

We previously looked at how to create a keymap that makes it quick and easy to navigate between Markdown headings. Although that tip was simple to create, there is potential for false-positives. In this tip we take a look at how to implement the same keymap with Treesitter so that we can more-accurately target Markdown headings.

Create a markdown directory under ftplugin if it doesn't already exist, then add a file containing the following keymaps:

local ts_utils = require("nvim-treesitter.ts_utils")

local M = {
-- define the query
query = vim.treesitter.query.parse("markdown", "((atx_heading) @header)"),
}

M.init = function()
-- search the current buffer
M.buffer = 0

-- references to lines within the buffer
M.first_line = 0
M.current_line = vim.fn.line(".")
M.previous_line = M.current_line - 1
M.next_line = M.current_line + 1
M.last_line = -1

-- default count
M.count = 1

if vim.v.count > 1 then
M.count = vim.v.count
end

-- list of captures
M.captures = {}

-- get the parser
M.parser = vim.treesitter.get_parser()
-- parse the tree
M.tree = M.parser:parse()[1]
-- get the root of the resulting tree
M.root = M.tree:root()
end

M.next_heading = function()
M.init()

-- populate captures with all matching nodes from the next line to
-- the last line of the buffer
for _, node, _, _ in
M.query:iter_captures(M.root, M.buffer, M.next_line, M.last_line)
do
table.insert(M.captures, node)
end

-- get the node at the specified index
ts_utils.goto_node(M.captures[M.count])
end

M.previous_heading = function()
M.init()

-- if we are already at the top of the buffer
-- there are no previous headings
if M.current_line == M.first_line + 1 then
return
end

-- populate captures with all matching nodes from the first line
-- of the buffer to the previous line
for _, node, _, _ in
M.query:iter_captures(M.root, M.buffer, M.first_line, M.previous_line)
do
table.insert(M.captures, node)
end

-- get the node at the specified index
ts_utils.goto_node(M.captures[#M.captures - M.count + 1])
end

-- define the keymaps
vim.keymap.set("n", "<A-j>", M.next_heading)

vim.keymap.set("n", "<A-k>", M.previous_heading)

This is a bit more complicated, but still fairly simple. We start by creating a lua module, followed by an initialization function that collects information about the buffer, cursor, and count. Finally, separate functions are created to implement jumping in the forward and reverse directions, respectively. Finally, we create the keymaps.

As a quick demo, let's follow the same steps we did in the previous tip. Starting from the top, in steps 1 and 2 we hit <A-j> to step forward through the headings, then finally in step 3 we use <A-k> to jump back up to the previous heading:

Before
#·Heading·1
 
Text
 
##·Heading·2
 
Text
 
###·Heading·3
NORMAL
Top
1:1
 
Jump to Next Heading
<A-j>
#·Heading·1
 
Text
 
##·Heading·2
 
Text
 
###·Heading·3
NORMAL
50%
5:1
 
Jump to Next Heading (Again)
<A-j>
#·Heading·1
 
Text
 
##·Heading·2
 
Text
 
###·Heading·3
NORMAL
90%
9:1
 
Jump to Previous Heading
<A-k>
#·Heading·1
 
Text
 
##·Heading·2
 
Text
 
###·Heading·3
NORMAL
50%
5:1