Package Control

The official Sublime Text package manager.

Markdown Nav

Navigate a long markdown document by its headers and its tags, from the Command Palette

By noahcoad
Created September 9, 2026, last updated September 9, 2026.

AugJulJunMayAprMarFebJan Dec2025 NovOctSep 2026-W38 | installs: 4 | removals: 0 | upgrades: 0 2026-W38 | installs: 4 | removals: 0 | upgrades: 0 2026-W38 | installs: 4 | removals: 0 | upgrades: 0 024 WeeklyInstalls 024 WeeklyUpgrades 2026-W37 0.1.0
Installations: 4 in total
Installations minus removals
Upgrades

Links

Version

0.1.0 (>ST4106) ·
2026-09-09 05:07

Markdown Nav

Navigate a long markdown document by its headers and its tags, from the Command Palette.

Built for big notes files — the kind that accumulate for years and get too long to scroll. Two commands:

Command Palette What it does
Markdown Nav: Jump to Header Walk the header tree one level at a time to the section you want
Markdown Nav: Find Tag Pick a tag, then pick one of its matches, then jump there

No key bindings are set by default. Copy what you want out of Example.sublime-keymap, via Preferences → Package Settings → Markdown Nav → Key Bindings.

Jump to Header

Rather than one flat list of every header in the file, this descends the outline a level at a time:

Each row shows the header's depth (##), its text, its line number, and how many sub-headers it has. Levels may be skipped: an ## H2 whose children are #### H4s works fine — each panel shows the shallowest level actually present in that section. Headers inside fenced code blocks are ignored. esc at any level cancels without moving the caret.

The panel reopens on your last pick. The top panel highlights the section containing it, and each level down highlights the next step along that path, so re-running the command puts you right back where you were. Sublime's quick panel can't be pre-filled with filter text, so this is done with a pre-highlighted row — type to filter as usual, or press enter immediately to re-jump. The memory is per view and keyed on header text (so edits above a header don't lose it), and lasts only for the session.

The major header level

The first panel keys off the major level: the shallowest header level that appears more than once. A file with several # H1s has H1 as its major level; a file with a single # H1 document title falls through to its ## H2s, since a lone title isn't a section divider.

Find Tag

Two panels: pick a tag (listed most-recently-used first, with a match count), then pick a match (with its line number), then jump. Both are fuzzy-filterable like any Sublime quick panel.

Match labels are the tagged text — what the line says with the tag itself stripped out — so the list reads as content rather than as a column of repeated tag names.

How tags are defined

A tag has two orthogonal aspects: the form it's written in, and the scope it refers to. A tag's identity is its lowercased name.

Forms

Keyword-prefix form — heather: …

A line whose first text content (optionally after a markdown header prefix #{1,6} ) is <name>: followed by at least one space and then non-whitespace. The keyword is the tag.

The required space after the colon is what disqualifies URLs and similar:

Matches Does not match Why not
heather: lunch on Saturday mobilenotes://abc123 no space after :
**heather:** lunch http://example.com no space after :
**heather**: lunch mailto:foo@bar.com no space after :
issue: bad bearings key:value no space after :
## person: Heather Heather: lunch uppercase — see below

Hashtag form — #heather

A #name token anywhere on the line. It must be at the start of the line or preceded by a non-word, non-& character, so word#tag and the HTML entity &#8212; don't match.

Matches Does not match Why not
#heather #Heather uppercase — see below
**#heather** #123 numeric
## topic #heather &#8212; HTML entity
we met #heather today word#tag no boundary before #

Case. Tags are lowercase by default, in both forms. This is deliberate: it keeps tag identity unambiguous and stops every capitalized word before a colon (Note: …, TODO: …) and every sentence starting with a capital from becoming a tag. Change it with tag_name_pattern below if you'd rather have uppercase count.

Bold. Tags in either form may be wrapped in **…**; the markers are stripped before the tag is identified, and they don't affect scope.

Scopes

A tag's scope is section if either the line is a markdown header, or the line consists only of hashtag tokens. Otherwise it's single-line.

Example line Form Header? Tags-only? Scope
heather: lunch on Saturday keyword no no single-line
## heather: lunch keyword yes section
do #heather today hashtag no no single-line
#heather hashtag no yes section
#heather #family hashtag ×2 no yes section
## title #heather hashtag yes section

Ignored

Match text

The label shown for a match is the line with the tag stripped out:

Raw line Tag Label
heather: lunch on Saturday heather lunch on Saturday
**heather:** lunch on Saturday heather lunch on Saturday
* heather: lunch heather lunch
5. heather: lunch heather lunch
## person: Heather person Heather
we met #heather today heather we met today
* #heather alone item heather alone item
* item with #heather inline heather * item with inline
## title #heather heather title

The rules: strip the tag and its bold markers; if the tag was at the start of the line, strip the leading whitespace, bullet marker (*, -, +, N., N)) and header marker too; if the line is a header, strip the header marker regardless of where the tag was; if the tag was mid-line, remove only the tag and keep the surrounding text as context; then collapse and trim whitespace.

When stripping leaves nothing, the line was tag-only — which means section scope — so the label comes from the section anchor instead, or (top of doc) if there's no preceding header.

Settings

Preferences → Package Settings → Markdown Nav → Settings, or Preferences: Markdown Nav Settings in the Command Palette.

{
	"tag_name_pattern": "[a-z][a-z0-9_-]*",
	"skip_frontmatter": true
}

tag_name_pattern — the character pattern for a tag name, spliced into both forms. To also accept uppercase, use "[A-Za-z][A-Za-z0-9_-]*". To allow dots, "[a-z][a-z0-9._-]*". It must not contain a capture group — use (?:…) if you need grouping — because the tag name has to stay as group 1 for scope and label extraction to work. A pattern that won't compile, or that adds a group, is ignored with a note in the status bar and the default is used instead.

skip_frontmatter — whether to skip a leading YAML --- block when scanning for tags.

Everything else about tag handling is structural rather than a preference, and lives in code: which of the two forms a line uses, whether the scope is a line or a section, where a section's anchor is, and how a label is stripped. Those are interdependent — the patterns carry a contract that group 1 is the tag name — so exposing them as raw regex settings would let a bad override break scope and labels silently instead of failing loudly. tag_name_pattern is the one part that varies independently of the rest.

Requirements

Sublime Text 4, build 4107 or newer (it runs in the Python 3.8 plugin host).

Layout

mdnav.py                       # both commands and the parsing helpers
Markdown Nav.sublime-settings  # tag_name_pattern, skip_frontmatter
Default.sublime-commands       # Command Palette entries
Main.sublime-menu              # Preferences -> Package Settings -> Markdown Nav
Example.sublime-keymap         # suggested bindings; Sublime never loads this file
tests/test_tags.py             # standalone, stubs `sublime`, no ST needed

Command names are mdnav_jump_to_header and mdnav_find_tag — the mdnav_ prefix predates the rename from "mdnav" to "Markdown Nav" and is kept so existing key bindings keep working.

Tests

py tests/test_tags.py

License

MIT — see LICENSE.

Results

Packages