keys: headings are a key away, read or written

`]]` and `[[` jump to the next and previous heading of the open document,
in the reader and in the editor, where vim's section motions mean nothing
useful in markdown. Past the last heading they go on to the end, as vim's
do. A heading a jump has just parked at the top counts as the one being
read, so pressing again moves on.

`gO` (or `:outline`, `:toc`) lists the headings in an overlay that opens on
the section being read or the one the cursor is in: `j`/`k` walk it,
`Enter` jumps there. It is the margin's outline for when the pane is too
narrow to show one, and for hands that stay on the keys. In the editor the
list comes from the `#` lines of the text, code fences left out, and a pick
moves the cursor; in the reader it comes from the rendered headings,
markdown files included.
This commit is contained in:
nvrl committed 2026-10-20 13:52:24 +02:00
1 parent 007fb1d3e3
commit 6a8ffdf408
10 files changed
+368 -10

No files matched your search

+20 -1
View File
@@ -1,6 +1,6 @@
import { api, ApiError, isContainer, type BackupLogin, type BackupStatus } from './api'
import { workspace } from './store.svelte'
import { formatDay, parseDay, parseDue, relative, sealedNote, type ScrollAmount } from './util'
import { formatDay, parseDay, parseDue, relative, sealedNote, type Heading, type ScrollAmount } from './util'
export type { ScrollAmount }
@@ -22,6 +22,11 @@ export const hooks = {
scrollReader: (_amount: ScrollAmount): boolean => false,
/* Filled in by the file pane: what Enter does there, false when nothing. */
fileAction: (): boolean => false,
/* `]]` / `[[` in whatever is being read; false when it has no headings. */
jumpHeading: (_direction: 1 | -1): boolean => false,
/* `gO`: the headings of what is on screen, read or being written, and the
one the reader or the cursor is in. */
headings: (): { headings: Heading[]; current: number } => ({ headings: [], current: 0 }),
/** How many space cards the overview fits in a row, for j/k to step by. */
overviewColumns: (): number => 1,
openOverview: () => {},
@@ -762,6 +767,20 @@ export const commands: CommandSpec[] = [
workspace.overlay = 'media'
}),
},
{
name: 'outline',
aliases: ['toc'],
group: 'documents',
help: "the open document's headings to pick from — gO, in the editor too",
run: () => {
if (!requireOpen()) return
const { headings, current } = hooks.headings()
if (headings.length === 0) return workspace.say('no headings in this document')
workspace.outline = headings
workspace.outlineCursor = current
workspace.overlay = 'outline'
},
},
{
name: 'search',
aliases: ['se'],