MarkNow for VS Code
Write markdown the way you like it — raw source, rich text, or both side by side.
Straight from the .md files in your workspace. No import step, no sidecar, no second copy.
What it adds
VS Code already has a very good markdown source editor and a perfectly decent
preview. What it does not have is a way to edit markdown as formatted text —
to select a word and press Ctrl+B and see it go bold, to type /table and get
a table, to click a Mermaid diagram and edit it.
That is what this is. It opens your real .md file in an editor tab, backed by
the same TextDocument VS Code itself uses, so everything else keeps working:
Ctrl+S, the dirty dot, hot exit, undo, Local History, the Timeline view, git
decorations, Search & Replace across files, and every other extension you have.
Three views, one document.
| Mode |
Shortcut |
What it is |
| Raw |
Ctrl+Shift+1 |
Syntax-highlighted source with a live preview column that scrolls with it. |
| Rich text |
Ctrl+Shift+2 |
Formatted text, styled like a page. Headings look like headings. |
| Dual |
Ctrl+Shift+3 |
Source and rich text side by side, editing the same document live. |
And a fourth, which is really the VS Code-shaped one: Ctrl+K V opens
MarkNow in the tab beside VS Code's own editor. Two editors, one file, both
live — the left one with multi-cursor, git gutters and every language feature
you have installed.
Getting started
- Open any
.md file.
- Click the preview icon in the editor title bar, or run MarkNow: Open in
MarkNow from the command palette, or right-click the file in the explorer.
- Write.
Nothing is imported, converted or copied. Ctrl+S writes the same file you
opened, in the same place, with the same line endings.
Make it the default (optional)
MarkNow deliberately does not take over the markdown editor. If you would
rather it did, tick Default editor for Markdown in MarkNow's menu, or run
MarkNow: Use as Default Markdown Editor from the Command Palette. Either
one applies to every project; untick it, or run MarkNow: Stop Using as
Default Markdown Editor, to hand markdown files back to the text editor.
Both write the setting VS Code reads, so you can also do it by hand:
// settings.json
"workbench.editorAssociations": {
"*.md": "marknow.editor"
}
A workspace that sets workbench.editorAssociations itself overrides your own
choice for the files it names; MarkNow says so when that is the case rather
than looking as though the switch did nothing.
You can always get back with MarkNow: Reopen with Text Editor (also in the
editor title bar).
What it understands
Baseline CommonMark, plus:
| Extension |
Syntax |
| Tables |
pipe tables, with optional column alignment |
| Task lists |
- [ ] / - [x], clickable in rich text |
| Strikethrough |
~~text~~ |
| Autolinks |
bare URLs |
| Footnotes |
[^1], rendered with backlinks |
| Math |
$inline$ and $$block$$, via KaTeX |
| Diagrams |
```mermaid fences, rendered and click-to-edit |
| Callouts |
> [!NOTE], GitHub/Obsidian style |
| Frontmatter |
leading --- YAML, editable in a metadata panel |
| Raw HTML |
preserved verbatim, rendered sanitized |
How faithful is it, exactly? Two different promises, deliberately:
- Raw mode is byte-faithful. Text you didn't touch is written back
unchanged. It will not re-wrap your paragraphs, re-order your link
definitions, or turn your
* bullets into -.
- Rich-text edits normalize style. Editing formatted text re-serializes the
document with consistent markers and spacing, following
marknow.markdown.*. Meaning, structure and content are never lost —
including syntax the rich editor cannot represent, which round-trips
verbatim. A 63-fixture golden corpus enforces this in CI.
Features
Rich-text editing. A fixed toolbar, / slash commands, drag handles,
markdown autoformat input rules, a code block with a language picker, and link
editing — none of it showing you a single *. Quick actions can also pop up
over a selection; they start off, so a selection stays unobscured until you
turn on marknow.wysiwyg.quickActions.
Tables that behave like a document's. Pick the size from a grid on the
toolbar. Enter starts a new line inside a cell — written as <br>, the only
line break a markdown table can hold — and Tab in the last cell adds a row.
Right-click any cell to insert, duplicate or delete rows and columns, or drag a
row or column by its grip. A table that ends the document keeps an empty line
below it, so there is always somewhere to click; the file never gains a byte
from it.
Diagrams and math. Mermaid and KaTeX render as you type. Click a diagram to
open a full-screen editor with the source on one side and a live preview on the
other; nothing is written back until you press Apply.
Review changes (Ctrl+Shift+D). A real diff of your unsaved work against
the file on disk: word-level highlighting, collapsed unchanged regions,
per-chunk revert, and Save right there in the header.
Images. Paste or drop an image and it is written into assets/ beside the
document as an ordinary file with a relative link — the way a static-site
generator wants it. No permission prompt, no media library. Configure the
folder with marknow.images.folder, or set it to "" to embed base64 instead.
Outline and counts. A heading tree you can jump from, and live word,
character and reading-time counts, with selection-scoped counts when you have
one.
Export. A single self-contained .html — styles, KaTeX fonts, Mermaid SVG
and local images all inlined, so it renders anywhere. Or copy the whole document
as rich text and paste it into Docs, Word or an email.
It follows your theme. Every colour comes from the active VS Code colour
theme, including high contrast. The source pane uses your editor font, your
selection colour and your find-match highlight.
Settings
All under marknow. in the Settings editor (MarkNow: Open Settings).
| Setting |
Default |
What it does |
defaultMode |
wysiwyg |
Mode a MarkNow editor opens in. |
raw.showPreview |
true |
Preview column beside the source in Raw mode. |
outline.visible |
false |
Heading outline sidebar. |
editor.fontSize |
0 |
Source-pane font size; 0 inherits editor.fontSize. |
editor.lineNumbers |
true |
Line numbers in the source pane. |
wysiwyg.measure |
72 |
Width of the rich-text column, in characters; the page's margins go around it. Also set by dragging. |
wysiwyg.quickActions |
false |
Quick-action toolbar over selected text. Off keeps the selection clear; the main toolbar has them all. |
markdown.bullet |
- |
Bullet marker written by rich-text editing. |
markdown.emphasis |
* |
Italic marker. |
markdown.strong |
* |
Bold marker. |
markdown.fence |
` |
Code-fence character. |
markdown.listItemIndent |
one |
List-item indentation. |
markdown.rule |
- |
Horizontal-rule character. |
markdown.tableAlign |
false |
Pad table cells so pipes line up. Off because aligning rewrites every row of every table on any edit. |
diagram.scroll |
false |
Let a too-wide diagram scroll instead of scaling down. |
images.folder |
assets |
Where pasted images go, relative to the document. "" embeds base64. |
images.base64WarnKb |
200 |
Warn above this size when embedding inline. |
sync.debounceMs |
300 |
How long editing pauses before edits land in the text document (and so in VS Code's undo stack). |
export.openAfterExport |
true |
Open the exported HTML afterwards. |
export.pdfPaperSize |
auto |
Paper for the direct PDF export: auto (from your region), a4 or letter. |
security.allowRemoteImages |
true |
Load https: images a document references. Off means a document cannot cause any network request. |
retainContextWhenHidden |
true |
Keep a background MarkNow tab alive, so scroll and undo survive tab switches. |
Commands
Every one of these is in the command palette under MarkNow, and rebindable
in the Keyboard Shortcuts editor.
| Command |
Default key |
| Open in MarkNow |
— |
| Open MarkNow to the Side |
Ctrl+K V (from a markdown text editor) |
| Reopen with Text Editor |
— |
| Switch to Raw / Rich text / Dual Mode |
Ctrl+Shift+1 / 2 / 3 |
| Toggle Raw-Mode Preview |
Ctrl+Shift+V |
| Toggle Outline |
Ctrl+Shift+O |
| Find in Document |
Ctrl+F |
| Review Unsaved Changes |
Ctrl+Shift+D |
| Edit Frontmatter Metadata |
— |
| Export to Standalone HTML |
— |
| Export to PDF |
— |
| Copy Document as Rich Text |
— |
| Show Keyboard Shortcuts |
Ctrl+/ |
Formatting shortcuts inside the editor are the usual set: Ctrl+B / Ctrl+I /
Ctrl+Shift+X, Ctrl+E for inline code (or a code block, on an empty line),
Ctrl+K for a link, Ctrl+Alt+1..3 for headings, and Ctrl+Shift+8 / 7 /
9 to make — or unmake — a bullet, numbered or task list. They apply only while
the editor has the keyboard, so the workbench keeps its own keys everywhere
else. Press Ctrl+/ for the full searchable sheet.
Ctrl+K is shared rather than taken: it opens the link editor when there is
something to link — text selected, or the caret inside a link — and is VS
Code's chord prefix otherwise, so Ctrl+K Z, Ctrl+K Ctrl+S and the rest
still work while you are typing.
Things worth knowing
PDF export borrows a browser you already have. A VS Code webview cannot
open a print dialog and the extension host has no renderer, so Export to PDF
renders the same self-contained HTML and prints it with an installed
Chromium-family browser (Edge, Chrome, Chromium) running headless — no window
opens, and nothing is downloaded. Paper size follows
marknow.export.pdfPaperSize, which defaults to your region's.
If no such browser is found, the export falls back to writing the HTML and
opening it in your default browser, where Print → Save as PDF lives.
Undo crosses two stacks. Inside MarkNow, Ctrl+Z is the editor's own
history. In VS Code's text editor it is the document's undo stack. Both act on
the same file, and each sees the other's changes as new edits. Edits are
debounced and minimal, so VS Code's stack gets word-sized entries rather than
one per keystroke.
Two MarkNow tabs on one file work, and stay in sync through the document.
If you type into both within the same debounce window, one is refused as stale
and re-synced — the host always wins, because silently reverting an edit that
already landed would be worse than losing a few keystrokes typed into a stale
buffer.
Restricted workspaces. In an untrusted workspace MarkNow never writes image
files and never resolves local image paths. Everything else works.
Remote images load. A document with an https: image URL causes a request
when opened, matching VS Code's own markdown preview at its default security
level. Plain http: never loads, and marknow.security.allowRemoteImages: false
stops even that.
Privacy
No accounts, no telemetry, no analytics, no network calls for any function of
the extension. Your documents never leave your machine. The only way a document
touches the network is if you put a remote image URL in it.
Development
npm ci # install
npm run build # extension bundle + webview bundle
npm test # unit, component and round-trip tests
Then F5 in VS Code to launch an Extension Development Host.
| Command |
What it does |
npm test |
Vitest, both suites (extension host in Node, webview in jsdom) |
npm run typecheck |
TypeScript, strict, both projects |
npm run lint |
ESLint |
npm run build |
Production build of both bundles |
npm run package |
Build a .vsix |
npm run release |
Check, build and package a release — publishes nothing |
npm run upstream:status |
What upstream MarkNow has that this port does not yet |
npm run upstream:next |
Replay the next upstream commit, ready for review |
Publishing to the Marketplace and Open VSX is in docs/publishing.md.
The sync scripts need Node 24 (see .nvmrc); everything else runs on Node 18+.
How it fits together
VS Code Webview
┌──────────────────────────┐ ┌──────────────────────────┐
│ TextDocument │ │ DocumentStore │
│ ▲ minimal TextEdits │ ready / init │ one markdown string │
│ │ │ ◄─────────────────► │ │
│ DocumentBridge ────────┼─ text / edit ──────►│ DocumentSync │
│ echo-safe, revisioned │ flush / flushed │ │
│ │ │ ├─ CodeMirror (raw) │
│ images · export · links │ ◄─── requests ──────┤ ├─ Milkdown (rich) │
│ settings · commands │ │ └─ unified/remark │
└──────────────────────────┘ └──────────────────────────┘
src/ webview/src/
The host owns everything with a path in it; the webview owns everything with a
grammar in it. Both sides break sync loops the same way — by comparing content
against the text they last agreed on, never by tracking who caused what.
Credits
A port of MarkNow, the local-first web editor, by
the same author, and kept in step with it. The markdown grammar, the rich-text
editor and the round-trip corpus come across unchanged; the file layer, the
theming and the chrome are rebuilt for VS Code.
License
MIT.