VS Code extension for working with markdown checklists. Renders markdown (markdown-it, the same engine as the built-in preview) with clickable checkboxes; every toggle is mirrored surgically into the source file.
- Preview text is selectable and copyable (prose, code, tables); copying yields the rendered text, not the markdown source
- Click directly on a checkbox to toggle it; a click elsewhere in the task row (the label) toggles only when no text is selected and it is a single click, so selecting or double-clicking the label text never toggles
- Ctrl+Click / Shift+Click on a checkbox selects multiple tasks; clicking a checkbox inside the selection toggles all selected tasks in parallel, as a single WorkspaceEdit (one undo step). The batch gestures live on the checkbox so Shift in the label stays normal text selection
- Ctrl+F opens VS Code's find widget over the rendered text (highlight, next/previous, match count) when the preview or workbench editor is focused
- Toggles replace exactly one character (
[ ]<->[x]); whitespace, HTML comments and everything else stay byte-identical - Supported markers:
- [ ],* [ ],+ [ ],1. [ ], nested, and the compound form1. - [ ](a numbered item whose content is a one-line bullet task) - both forms toggle and continue on Enter alike - Editing-oriented rendering, deviating from the built-in preview
(docs/DECISIONS.md #25): numbered task items keep their visible number
(mixed lists count without gaps), and
[ ]/[x]without a label renders as a clickable task row instead of literal text - so fresh Enter-continuation lines don't flicker while typing [ ]/[x]inside table cells render as clickable checkboxes too, toggled surgically by line + occurrence (direct toggle, not part of multi-select; header row excluded). If a cell contains exactly one checkbox, clicking anywhere in the cell toggles it - gated like the task row, so selecting the cell text does not toggle.- Esc clears the selection
- Preview panel (
Open Workbench/Open Workbench to the Side): opens next to or in place of the active group; the source file stays open and the panel closes independently. One panel per document; closes automatically when the source document is closed. - Custom editor (
Open as Workbench): swaps the active editor in-place in the same tab (reopenActiveEditorWith, like the built-in); "Reopen as source file" swaps back in that tab. Also reachable via "Reopen Editor With...". - Both modes mark their tabs with the workbench icon and a "Workbench: " title, like the built-in preview marks its tabs.
Menu placement mirrors the built-in preview: two icon buttons at the end of
the tab row (the split-workbench glyph opens to the side, Alt held switches it
to the workbench glyph that opens in the active group), tab context menu
entries in group 1_open, and an explorer context entry.
- Full markdown via markdown-it (
html: true,linkify: true): tables, links, images, blockquotes, nested lists, fenced code - Syntax highlighting via shiki with the VS Code
dark-plus/light-plusthemes, following the active color theme kind (re-renders on theme switch). Preloaded languages: powershell, bat, shellscript, json, jsonc, yaml, ini, xml, javascript, typescript, html, css, markdown, csharp, python, sql, diff, docker. Unknown languages fall back to plain blocks. - YAML frontmatter (
---block at file start) renders as a property card: flatkey: valuepairs become a key/value grid, anything nested falls back to a raw monospace card - HTML comments are hidden in the view and preserved in the source
- Theme-aware styling from
--vscode-*tokens: configurable centered measure (settingmarkdownWorkbench.preview.maxWidth:github= 980px default,narrow= 72ch; applies live), hairline borders, rounded code blocks; tables with horizontal hairlines only, uppercase muted sticky headers (column labels stay visible while scrolling long tables), zebra striping and row hover
An editor-style minimap rail shows a scaled clone of the rendered content
with a draggable viewport slider (editor minimap theme tokens); the slider
can be grabbed and dragged like the editor minimap (no jump on grab), a
click on the rail outside it jumps and centers, and the rail hides
automatically when the document fits the viewport.
Configurable like the editor minimap via markdownWorkbench.minimap.*:
enabled, size (proportional pans for long documents, fill maps the
document linearly onto the rail so the slider never drifts from the
scrollbar, fit downscales without stretching), showSlider (mouseover
default / always), and side (right / left). Changes apply live.
Three settings tune how selectable text and the task toggle coexist (all apply live; the defaults reproduce the 0.30.0 behavior):
markdownWorkbench.preview.textSelection(defaulttrue): preview text is selectable and copyable. Setfalseto lock selection (as before 0.30.0) - a click anywhere in a task row then toggles it, ungated.markdownWorkbench.preview.taskBatchSelect(checkboxdefault /row): where the Shift/Ctrl multi-select fires.checkboxkeeps it on the checkbox so Shift in the label stays a normal text selection;rowfires it anywhere in the row (the price: Shift in the label no longer extends a selection).markdownWorkbench.preview.taskRowTextCursor(defaultfalse, only whentextSelectionis on): show a text caret over a task row label so it reads as selectable; the checkbox keeps the pointer hand.
To restore the pre-0.30.0 behavior (no selectable text, the whole row toggles
and carries the batch gesture), set textSelection: false and
taskBatchSelect: row.
Bidirectional and pixel-accurate between the view and any visible text
editor of the same document, using the built-in preview's fractional-line
algorithms: positions interpolate between data-line mapped elements
(markdown-it token maps), multi-line code fences scroll proportionally, and
echo suppression works in both directions. Opening any view jumps straight
to the source editor's position; the way back restores the synced position.
In the text editor (not the view), pressing Enter inside a list item inserts the next marker:
- foo+ Enter ->-on the next line- [x] foo+ Enter ->- [ ](always unchecked)3. item+ Enter ->4.(delimiter preserved:3)->4);3. [x] foo->4. [ ])- Enter in the middle of a numbered sequence renumbers the following siblings of the same level and delimiter, so the source stays readable
- Indentation is preserved; Enter on an empty item removes the marker (terminates the list)
- Enter on a continuation line (a wrapped or Shift+Enter-hung line, see below) continues its item too: a fresh sibling at the item's level, with the following siblings renumbered as usual - including when the continuation line sits below deeper-indented children of the item (the next sibling is still created at the parent's level)
Shift+Enter inside a list item, or on one of its continuation lines, breaks the line and indents the new one with whitespace to the item's content column
-
markerless, no number, so the text hangs aligned under the item's text:
-
2.+ Shift+Enter -> a new line indented by 3 spaces (under2.) -
- [ ]+ Shift+Enter -> indented by 9,1. - [ ]likewise -
Text right of the cursor moves down onto the new line
Outside a list - or with the cursor still inside the marker/indentation - Shift+Enter falls through to the editor default. Because the hung lines are markerless and indented to the content column, Enter afterwards still counts the sequence correctly - the same shape external reflow extensions (e.g. marvhen.reflow-markdown, Alt+Q) produce when they wrap long list items.
On list lines, Tab indents and Shift+Tab outdents (multi-line selections
supported). The indent unit is adaptive per CommonMark: marker + gap width,
so - nests by 2 and 10. by 4. Non-list lines fall through to the
default Tab/outdent; Tab keeps working for suggest, snippets and inline
suggestions via the when clause.
A single numbered item starts a new sublist on Tab; if the deeper level
already has a preceding sibling the item joins its sequence (number = next
after that sibling), otherwise it restarts at 1 (delimiter preserved), so
tabbing several items into the same sublist numbers them 1. 2. 3.
instead of leaving duplicate markers. Shift+Tab joins the target-level
sequence (number = next after the preceding sibling there). In both
directions the sequence left behind closes its gap, and Shift+Tab also
renumbers the target sequence. Dash items under numbered parents (and vice
versa) are never rewritten - each level keeps its list type.
On a markerless line (not a list item - a wrapped or hung continuation line,
or plain text), Tab/Shift+Tab instead snap the line's indentation onto a column
stop: column 0, the indent/content columns of nearby list items, every word
start of nearby lines, and the editor's tab-size multiples. Tab moves to the
next stop to the right, Shift+Tab to the next to the left (so a continuation
line can be aligned under a word above it, not just by a fixed step); with no
detected stop nearby it steps by the tab size, so a forward step is always
available. markdownWorkbench.indent.continuationStopRadius (default 5) sets
how many lines above and below are scanned for stops. List-item lines are
unaffected by this - they keep the structural nesting/renumbering above.
When more than one line is selected, the whole selection - list items and markerless lines together - moves as a block by one common delta, so the relative indentation is preserved and nothing drifts apart: the topmost selected line snaps to its next stop and every line shifts by that amount. A Shift+Tab left shift is capped by the flattest line in the selection, so nothing slides below column 0. Markers are not renumbered in a multi-line selection - it is pure indentation (a single list item still nests and renumbers structurally, as above). A single markerless line snaps to its own stop.
When you change a numbered marker by hand (e.g. type 2. over to 5.), the
following siblings of the same level continue from it - 1. a / 5. b / 6. c.
The sequence follows your input; it is never reset to 1, so a list may start
at any number. Only editing the marker triggers it - editing a line's text
leaves an intentionally non-sequential list alone. This runs behind the same
guard as Enter/Tab/Shift+Tab, so those structural edits do their own
renumbering without the manual pass firing on top. (For custom markers, changing
the first item of a level propagates the type to its siblings, as above.)
Ordered lists render with classic outline markers by depth: 1. on level 1,
a. on level 2, i. on level 3, repeating from level 4. Only ol levels
count, and each level renumbers for itself. The markers are pure preview
styling - the source always keeps portable CommonMark digit markers
(1. / 1)), never letters.
Two mirror-image joins, each off by default and bound only when its setting is on:
- Ctrl+Delete (
markdownWorkbench.editing.forwardJoin.enabled): at the end of a line's visible content, merge it with the next line that has content. - Ctrl+Backspace (
markdownWorkbench.editing.backwardJoin.enabled): at the start of a line's visible content, append it to the previous line that has content.
They also work with the cursor on an empty (or whitespace-only) line - it counts as being at both the line's end and start, so Ctrl+Delete pulls the next content line up to the cursor and Ctrl+Backspace moves the cursor to the end of the previous content line.
Both delete any blank or whitespace-only lines in between (the next content line
is pulled in even across empty lines, and it need not be indented), and they
normalize the seam: existing trailing/leading whitespace and the removed line
breaks become exactly markdownWorkbench.editing.joinSpaces spaces (default 1;
set 0 to join with no space) - never a double space. The join spaces are added
only when both sides have visible content; joining onto or from an empty line
adds no space (the texts meet directly). In any other position - or
when there is no content line to join - each runs its fallback command
(forwardJoin.fallbackCommand, default deleteWordRight;
backwardJoin.fallbackCommand, default deleteWordLeft), executed directly so
it is safe even when bound to the same key.
Note: a personal ctrl+delete / ctrl+backspace keybinding with no when
clause overrides the workbench binding. To keep both, scope your own binding
with when: editorLangId != markdown - otherwise the workbench handler never
fires in markdown editors.
Turn on markdownWorkbench.lists.extraMarkersEnabled and list the markers in
markdownWorkbench.lists.extraMarkers (both required; off by default) to let
the editor treat extra, non-CommonMark markers as list items. Pick from a closed
set: symbol bullets ->, →, ❯ (repeat, like dashes); lettered markers
a), A), a., A., a:, A: (count up a, b, … z, za; upper-case kept
separate; the delimiter is preserved); and digit markers 1), 1: (count
like numbers, : included). Enter continues them, and Tab/Shift+Tab nest and
renumber them with the same machinery as native numbered lists.
- On Tab, the deeper level's marker comes from
markdownWorkbench.lists.markerCycleby depth (default1.→a)→1)→a., cycling), unless a sibling already sits at that level - then its sequence continues. A symbol item keeps its bullet (symbols just repeat). Typing a different marker overrides it from there on. - Tab/Shift+Tab renumber lettered and digit sequences just like numbers: the
level left behind closes its gap (
a) b) c) d), Tab onc)leavesa) b) c)), and Shift+Tab joins the target level's sequence, adopting its family (a1)moved up under ana)list becomesb)). Only the marker token is rewritten, so a multi-space gap after it is preserved. - Changing the marker type of the first item of a level pulls its
same-level siblings to the new type and sequence (
a) b) c)with the first set to1)→1) 2) 3)); child and parent levels are never touched. - Because these markers are not CommonMark, an enabled letter/digit family can
also match ordinary prose at the start of a line (e.g.
ok) gooris: thiswitha)/a:on). Recognition - and thus Enter continuation and Tab/Shift+Tab - then applies on those lines too. This is inherent to opting in; the two-character letter bound keeps it to short tokens (note:/foo)with three or more letters are not matched), but 1-2 letter collisions remain. - These markers are a deliberate deviation from CommonMark, meant for working
notes. The source stays portable: with
markdownWorkbench.lists.renderExtraMarkerson (and only then), the preview renders these lines as lists with the same outline styling as native lists; everywhere else (GitHub/GitLab/Forgejo), and with the setting off, they remain plain text. Nesting renders cleanly when every level uses a non-CommonMark marker; levels written with native markers (1.,1)) stay separate native lists.
- Typing the language after ``` (or ~~~) pops IntelliSense with the bundled shiki languages and common aliases (ps1, bash, sh, yml, js, ts, batch)
- Enter at the end of an unclosed opening fence inserts the closing fence and puts the cursor on the empty line in between (delimiter and indentation preserved; already-closed fences get a normal newline)
Modeled on the Learn Markdown bindings:
| Key | Action |
|---|---|
| Alt+D B / I / C | Toggle bold / italic / inline code (wraps selection or word under cursor, unwraps when already wrapped) |
| Alt+D K | Insert web link [text](url) as snippet with tabstops |
| Alt+D L | Insert relative link to a workspace file (quick pick) |
| Alt+M | Authoring menu with all commands below |
| Alt+P | Toggle Workbench to the Side (close when open; also closes a focused panel) |
Menu/palette only: Bulleted / Numbered / Task list (prefixes the selected
lines or inserts a marker), Insert Table (size prompt, snippet with
tabstops), Evenly Distribute Table / Consolidate Table (reflows the table at
the cursor or in the selection, keeps :---: alignment markers), Sort
Selection Ascending/Descending (numeric-aware), Insert Language Identifier
(quick pick over the bundled shiki languages).
Note: other extensions that also bind Enter/Tab or Alt+D for markdown (e.g. Learn Markdown, Markdown All in One) conflict with this — keep only one such handler enabled.
| Command | Title | Binding |
|---|---|---|
markdownWorkbench.showPreview |
Open Workbench | tab context, explorer context, Alt-variant of tab-row button |
markdownWorkbench.showPreviewToSide |
Open Workbench to the Side | tab-row icon |
markdownWorkbench.open |
Open as Workbench | tab-row icon, tab context |
markdownWorkbench.formatBold / formatItalic / formatCode |
Bold / Italic / Code | Alt+D B / I / C |
markdownWorkbench.insertWebLink / insertFileLink |
Link to Web / File | Alt+D K / L |
markdownWorkbench.authoringMenu |
Markdown Authoring Menu | Alt+M |
markdownWorkbench.insert*List, insertTable, distributeTable, consolidateTable, sort*, insertLanguageIdentifier |
see authoring menu | palette / Alt+M |
markdownWorkbench.onEnterKey / onTabKey / onShiftTabKey |
(internal) | Enter / Tab / Shift+Tab in markdown editors |
markdownWorkbench.joinForwardOrFallback |
Join Next Content Line | Ctrl+Delete (only when editing.forwardJoin.enabled is on) |
markdownWorkbench.joinBackwardOrFallback |
Join With Previous Content Line | Ctrl+Backspace (only when editing.backwardJoin.enabled is on) |
Untitled files: the *.md selector does not match untitled documents, so use
the command palette ("Open as Workbench" / "Open Workbench...") while the
untitled tab is active.
Publishing to the VS Code Marketplace is planned as the future install path (link follows after the first publish). Until then, install the vsix from the Releases page:
Download the latest .vsix from the
Releases page (every
green build on main publishes one), then:
code --install-extension markdown-workbench-<version>.vsixEach release ships the vsix as a direct download plus SHA256SUMS.txt, and
the vsix carries a build-provenance attestation. To verify it came from this
repo's CI before installing:
gh attestation verify markdown-workbench-<version>.vsix --repo ww3d/markdown-workbenchnpm install
npx @vscode/vsce packageNo build step; plain JavaScript. Dependencies: markdown-it, markdown-it-front-matter, shiki.
npm ci
./build.ps1 # version check + coverage gate + packageSee CONTRIBUTING.md for the workflow, docs/ARCHITECTURE.md for how the
pieces fit together and docs/DECISIONS.md for the decision log including
rejected approaches.