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
NORMALTop1:1
 
Jump to Next Heading<A-j>
#·Heading·1
 
Text
 
##·Heading·2
 
Text
 
###·Heading·3
NORMAL50%5:1
 
Jump to Next Heading (Again)<A-j>
#·Heading·1
 
Text
 
##·Heading·2
 
Text
 
###·Heading·3
NORMAL90%9:1
 
Jump to Previous Heading<A-k>
#·Heading·1
 
Text
 
##·Heading·2
 
Text
 
###·Heading·3
NORMAL50%5:1