Skip to content

ww3d/markdown-workbench

Repository files navigation

Markdown Workbench

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.

Features

Workbench view

  • 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 form 1. - [ ] (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

Two modes (mirroring the built-in markdown preview)

  • 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.

Rendering

  • 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-plus themes, 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: flat key: value pairs 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 (setting markdownWorkbench.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

Minimap

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.

Preview readability settings

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 (default true): preview text is selectable and copyable. Set false to lock selection (as before 0.30.0) - a click anywhere in a task row then toggles it, ungated.
  • markdownWorkbench.preview.taskBatchSelect (checkbox default / row): where the Shift/Ctrl multi-select fires. checkbox keeps it on the checkbox so Shift in the label stays a normal text selection; row fires it anywhere in the row (the price: Shift in the label no longer extends a selection).
  • markdownWorkbench.preview.taskRowTextCursor (default false, only when textSelection is 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.

Scroll sync

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.

List continuation on Enter

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)

Hanging continuation lines on Shift+Enter

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 (under 2. )

  • - [ ] + 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.

List nesting on Tab / Shift+Tab

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.

Auto-renumber on manual edits

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 list outline in the view

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.

Join content lines on Ctrl+Delete / Ctrl+Backspace (opt-in)

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.

Custom list markers (opt-in)

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.markerCycle by depth (default 1.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 on c) leaves a) b) c)), and Shift+Tab joins the target level's sequence, adopting its family (a 1) moved up under an a) list becomes b)). 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 to 1)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) go or is: this with a) / 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.renderExtraMarkers on (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.

Code fences

  • 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)

Authoring shortcuts (Alt+D chords, Alt+M menu)

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.

Commands

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.

Install (local)

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>.vsix

Each 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-workbench

Build from source

npm install
npx @vscode/vsce package

No build step; plain JavaScript. Dependencies: markdown-it, markdown-it-front-matter, shiki.

Development

npm ci
./build.ps1            # version check + coverage gate + package

See 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.

About

Interactive markdown preview for VS Code with toggleable checkboxes, minimap, modern tables and authoring tools. Toggles are mirrored surgically into the source file.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors