All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog,
and this project adheres to Semantic Versioning.
Until the package reaches 1.0.0, minor versions may include breaking API
changes as the public surface stabilizes.
linkWidgets(...specs), a host seam on the engine's own link decorator. EachLinkWidgetSpechas a synchronousmatch(link: LinkWidgetLink): WidgetType | null, whereLinkWidgetLinkis{ url, text, title?, from, to }. For a single-line inline[text](url)link whose syntax would be hidden (not revealed by the caret, focus, the pointer-press freeze, or a diff change), the first non-null widget replaces the whole link range instead of thecm-atomic-linkmark and the hidden-syntax replaces: no underline, no external-link icon, no link-opener hit zone. Revealed links, multi-line links, images, links whose text holds an image, reference links, autolinks and wiki links are unchanged.eqis the widget author's: the engine asksmatchon every rebuild. A spec whosematchthrows is logged once and skipped, and the next spec is asked.refreshLinkWidgets, aStateEffectType<null>. An effect-only transaction carrying it rebuilds link decorations and asksmatchagain, with no document change and no history entry, for hosts whose answer changes after mount. A reconfiguredlinkWidgets()facet also rebuilds. While a pointer press holds the preview frozen the rebuild waits for the release.- Demo harness
?mode=link-widgetsand Playwright probes (npm run test:e2e) for the real press, click, keyboard-entry and blur paths on a drawn widget.
- Links inside table cells are drawn by the table widget's own cell
renderer, which edits cell text as contenteditable DOM rather than
through CodeMirror decorations. They keep the link look in this release
and
matchis not called for them; offering them tolinkWidgetsspecs follows in a later release.
- The mount-time syntax tree is bounded to an initial window (16 KB or
the viewport, 20 ms budget) instead of a synchronous whole-document
parse. Tables, image blocks, and the inline preview share that policy
through one
decorationTreehelper. The tree-progress idle loop grows the tree one bounded segment per tick (8 KB doubling to 64 KB, reset on edits,requestIdleCallbackwith a 400 ms timeout) and dispatches a rebuild after each completed segment, so content past the window is decorated progressively rather than in one late rebuild. Decoration-only: document bytes, editing commands, undo history, and the public API are unchanged. Entering a 283 KB document no longer blocks the main thread on the parse. - New
npm run bench:entrybenchmark asserts the mount-time parse stays inside the window.
AtomicDiffEditor, a frozen unified Markdown review surface backed by@codemirror/merge: inline insertions/deletions, change navigation, gutter rails, a dedicated line-mapped change overview, full unchanged context, large-document diff safeguards, and the existing consumerextensionsseam.
- Built-in frontmatter, table, image, task, inline-preview, and wiki-link decorations now cooperate with unified diff state. Unchanged atomic content stays rendered; changed atomic ranges expose source so review evidence cannot be hidden by a replacement widget.
- The dedicated diff-overview column now preserves CodeMirror's bounded inner scroller instead of expanding the grid row to the document's full height.
- Table visual pass, Linear-style: rounded 8px container with interior
hairlines only, shaded header row (
--atomic-editor-bg-panel), roomier cells (10px 14px), and a subtle row hover wash. Rendering-only — raw markdown is untouched and stays single-line per cell.
preferResolvedLabelonWikiLinksConfig. With the flag on, labeled wiki links ([[target|label]]) resolve to and display the document's current title, falling back to the stored label while loading or when the target is missing — so renames propagate to every rendered link without rewriting document bytes. Purely a display preference; the source is never touched. Defaults to false (existing behavior unchanged).- Table hover affordances for discoverability: a
+on the right edge (append column), a+on the bottom edge (append row), and a⋯handle (top-right) that opens the existing table menu — all revealed on hover/focus, absolutely positioned so the widget's measured height and click routing below the table are unchanged. Edits flow through the existing model→serialize→dispatch paths (byte-identical to the context menu); right-click still works. Floating chrome uses the--atomic-editor-menu-*tokens with dark fallbacks.
Deliberately out of scope: column-width persistence (GFM has no width syntax), multi-line cell content, and row drag-reorder.
- Typing a new row directly beneath a rendered table no longer corrupts
the document (upstream bug): the instant lezer absorbed the just-typed
| … |line into the Table node, the atomic block widget grew over the caret's line, CM6's DOM selection lost its text position, and further keystrokes landed at a displaced position. A table now reveals its raw source while the caret sits on its last line (the caret always owns a real text position) and folds the finished row into the widget as soon as the caret leaves — the same reveal-at-cursor convention the inline preview uses. Selection-only caret moves rebuild the decorations only when entering/leaving a pipe-bearing line, so ordinary cursor motion costs nothing.
- Slash-command insert menu. Typing
/at the start of a line opens a keyboard-navigable menu of block insertions (headings, lists, task list, quote, code block, table, divider, link, image). Opt-in via the newslashCommands()extension factory; custom items and default replacement viaSlashCommandsConfig. - Slash menu redesigned — per-row icons, an inset rounded pill for the
hover/selected state, and card elevation. Fully themeable via seven new
tokens:
--atomic-editor-menu-bg,--atomic-editor-menu-border,--atomic-editor-menu-shadow,--atomic-editor-menu-radius,--atomic-editor-menu-item-hover-bg,--atomic-editor-menu-fg, and--atomic-editor-menu-fg-muted(dark fallbacks are built in). Each default command ships an icon;SlashCommandItem.iconsets a custom item's glyph (inline SVG usingcurrentColor), and items without one fall back to a default glyph so the icon gutter stays aligned. selectionToolbar()— an opt-in floating formatting bar (bubble menu) shown above a non-empty selection, with bold / italic / strikethrough / inline-code / link toggle buttons, active-state highlighting, a bundled keymap (Mod-b,Mod-i,Mod-Shift-x,Mod-e,Mod-k), and--atomic-editor-*theming. Configurable button set viaSelectionToolbarConfig.- Byte-exact inline formatting toggle commands (
toggleBold,toggleItalic,toggleStrikethrough,toggleInlineCode,toggleLink) plus the state-level helpersapplyFormat,getActiveFormats, andinlineFormattingAllowed. Unwraps delete exactly the marker bytes read from the syntax tree; wraps trim whitespace out of the marked range and refuse anything that would produce broken markdown (marker-boundary crossings, code/frontmatter contexts). - Multi-line selections toggle per line, Obsidian-style: the selection is split into whitespace-trimmed per-line segments; blank lines, code/ frontmatter lines, and table rows are skipped; if every eligible line is already formatted the toggle unwraps them all, otherwise it wraps the unformatted ones — all in a single transaction (one undo step). The toolbar now shows for multi-line selections; the link toggle stays single-line (its button is disabled across lines).
- In-cell formatting: selecting text inside a table cell shows the same toolbar chrome (bold / italic / strikethrough — the marks cells render; no code or link in cells) anchored to the DOM selection. Toggles rewrite only the selected span of the cell's raw markdown and flow through the widget's existing single-cell commit path, so serialization, pipe escaping, and undo behavior are unchanged.
- Toolbar chrome tokens: both bars share a Linear-style look themed via
--atomic-editor-menu-bg,--atomic-editor-menu-border,--atomic-editor-menu-shadow,--atomic-editor-menu-radius,--atomic-editor-menu-item-hover-bg,--atomic-editor-menu-fg, and--atomic-editor-menu-fg-muted(dark fallbacks inline; light values are the consuming theme's job). The active-format wash reuses--atomic-editor-accent-soft/ the accent family — no bespoke token.
- The selection toolbar could stay hidden after a drag that released over a block widget (e.g. the table) or any element that stops pointer-event propagation: the drag-suppression latch now clears in the capture phase.
- Tooltips no longer escape the editor: the toolbar bundles a
tooltips({ tooltipSpace })config clamping tooltip space to the editor's rect (intersected with the window), so a first-line selection flips the bar below instead of rendering over host chrome. This applies to every tooltip in the editor (autocomplete included) by design; a consumer's owntooltipSpaceregistered at higher precedence still wins. The in-cell bar flips below the selection near the top edge too. - Wiki-link suggestions regressed to never resolving after the language-data registration change: the completion source closure was recreated on every read, so CodeMirror's autocomplete treated each update as a new source and dropped in-flight async results. The source is now built once, with regression tests locking source identity for both wiki-links and slash commands.
wikiLinkssuggestions now register through language data instead of the autocompleteoverrideconfig, so they compose with other completion sources (likeslashCommands). As a side effect, nested code-language completions (e.g. HTML inside fences) can now surface while wiki-link suggestions are enabled.
- Explicit TypeScript support declaration:
typescriptis now an optional peer dependency at^5.0.0 || ^6.0.0. Note for the curious: 0.5.0 declared no typescript peer at all (no package manager warned on TS 6 — we checked npm, pnpm, and bun against the published manifest), so this release adds the declaration rather than widening one. Optional peers emit no warning when absent; consumers on TS 5 or 6 are equally supported.
First release under the @plannotator/atomic-editor name (Plannotator's
fork of @atomic-editor/editor; upstream base: 0.4.3).
- YAML frontmatter parsing: a leading
---block now parses as a dedicatedFrontmatternode and renders as a quiet monospace metadata block with faded fences. Previously the opening fence parsed as a horizontal rule and the YAML body plus closing fence as a setext H2. Document bytes are untouched; round-trips remain byte-identical. Adds@lezer/markdownas a peer dependency. - Frontmatter Properties widget (Obsidian-style). A parseable frontmatter
block renders as a key/value grid: in-place key and value editing, list
values as chips with add/remove, add/remove property rows, and an "edit as
YAML source" toggle. Edits dispatch single-line document changes — editing
one property never rewrites a neighboring line's bytes. YAML the grid can't
faithfully represent (nested maps, comments, block scalars, unclosed fence)
falls back to the styled raw text. Exported standalone as
frontmatterProperties().
Table-editing hardening. The WYSIWYG table widget is the most custom part of the editor, and its DOM ⇄ markdown round-trip and contenteditable cell handling were hiding several bugs.
- Insert column left/right now works. Inserting a column adds an empty
cell, and the table model counted columns from lezer
TableCellnodes — which lezer doesn't emit for empty cells — so the new column was dropped on re-render even though the document was updated. Columns are now counted by splitting the row's raw text, so blank columns survive the round-trip. - Typing a literal
|in a cell no longer corrupts the table. Cell content is now escaped on serialize (|→\|, newlines flattened), so a pipe can't split the row and shift/drop later columns. - IME and dead-key composition work in cells. The cell rebuilt its DOM on every input event, which cancelled an in-progress composition — dropping CJK input, accents, and dictation. Composition is now left alone until it ends.
- Clicking a styled run (bold/italic/link) in a cell keeps the caret where you clicked instead of jumping it to the end of the cell.
- The external-link icon in a cell opens its URL. It was a CSS
::afterpseudo-element, which has no event target, so clicking it dispatched no event. It's now a real element opened on click (a proper popup-activation gesture, sowindow.openisn't blocked). - Pasting into a cell now inserts a single line of plain text — pasted rich HTML, newlines, or pipes no longer land verbatim and corrupt the row.
- Enter in a cell now advances to the next cell (appending a row past the last one), mirroring Tab; Shift reverses direction. Previously it inserted a line break the single-line cell couldn't represent.
- Per-keystroke cell edits are tagged as input so the editor's undo history coalesces them into one step instead of one per keystroke.
- Typing into a heading (or other line with hidden syntax) immediately after
clicking it no longer crashes the editor. The inline-preview plugin freezes
decoration rebuilds during a mouse interaction so a clicked heading's
##prefix doesn't reveal mid-click and jitter. But while frozen it skipped the rebuild on doc changes too, handing CodeMirror a stale decoration set whose positions no longer matched the document — the##replace then spanned the newly-typed line break, throwingRangeError: Decorations that replace line breaks may not be specified via pluginsand corrupting the heightmap (No tile at position …, broken scroll-into-view, content "jumping"). The freeze now still rebuilds on document changes; it only suppresses the selection-driven reveal it was meant to.
--atomic-editor-selection-bgnow actually takes effect. CodeMirror's base theme styles the active selection with a deeper selector than the package used (&dark.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground), so the token was silently overridden by the default selection color. The rule now mirrors that selector depth (the same approachoneDarktakes), so the configured selection color applies in both themes.
TablesConfigis now exported from the package entry, so consumers passingonLinkClicktotables()can import the option type (the siblingInlinePreviewConfigand wiki-link types were already exported).- The light theme now defines
--atomic-editor-accent-softand--atomic-editor-initial-reveal-bg/-strong. These were referenced but unset under[data-theme="light"], so the blockquote rail and the reveal-on-arrival highlight previously borrowed dark-tuned values on a pale backdrop.
- Default link color shifted from a standalone blue to an indigo that
coordinates with the violet accent (
--atomic-editor-link#818cf8,--atomic-editor-link-hover#a5b4fc; light mode uses violet). Set those variables to restore any previous color. - Fenced code blocks now render with a subtle left rail so the block reads as a contained unit. The rail is an inset box-shadow, so line-box geometry (and CM6's height measurement) is unchanged.
- Inline-preview decorations are now built in a single syntax-tree walk per update instead of two, lowering the per-keystroke cost on large documents. No behavioral change.
- Mid-typing emphasis no longer flashes false italic inside intra-word
underscores (e.g.
snake_case_var), matching CommonMark's flanking rules. - The find panel's match counter now reads
9999+past its cap instead of a misleadingly exact count. - Wiki-link resolution results are now capped (LRU by insertion), so a long session that scrolls through many distinct targets no longer grows the cache without bound.
- Wiki-link extension for atom-style
[[...]]links. Consumers can now composewikiLinks()into the editor to render labeled wiki links, resolve bare targets asynchronously, open links from rendered text, and provide CodeMirror autocomplete suggestions. The extension supports custom serialization, resolver policies, debounced suggestions, and leaves draft links editable while the cursor is inside them. - Code-fence auto-close. Typing an opening triple-backtick fence now inserts the matching closing fence so a fence added in the middle of a note does not swallow all following content.
- Demo wiki-link deeplinks. The dev demo includes sample wiki-link suggestions, async resolution, and a lightweight deeplink readout for manual testing.
- Markdown link icon click behavior. Clicking the rendered external-link icon next to a markdown link no longer expands the raw markdown; only clicking the link text itself enters edit mode.
- Missing wiki-link Backspace behavior. Backspacing immediately after a
rendered missing bare link now first reveals the raw
[[...]]source, then normal Backspace edits inside the link instead of pulling the rendered link through preceding content.
- The dev server now binds to
0.0.0.0and accepts arbitrary dev hostnames, which makes package-level testing easier from LAN and tunneled environments.
- Crash on multi-line link / image titles. A markdown link or image
whose title wraps across lines — e.g.
[text](url "first\nsecond")— threwRangeError: Decorations that replace line breaks may not be specified via pluginsand took the editor down on mount. Root cause: the inline-previewViewPluginhides syntax tokens viaDecoration.replace, and CM6 forbids plugin-sourced replaces from crossing a newline (block / line-spanning decorations must come from aStateField). Lezer legitimately emits such nodes for wrappedLinkTitle/ image-title constructs. Every replace in the builder is now routed through apushReplacehelper that splits multi-line ranges into per-line segments; the first segment keeps any widget, so bullet / checkbox markers still render exactly once.
initialRevealTextprop +revealText(query)imperative method for arriving-from-search-result navigation. Scrolls the first match near the top of its scroll parent (handles editors embedded in a larger scrolling shell) and paints a 3.2 s fade-out highlight — no search panel, no cursor move, no lingering UI. Matcher falls back progressively (exact → whitespace-collapsed → individual lines → truncated prefixes at 140 and 80 chars) so hits resolve even when the query came from an LLM-massaged snippet that doesn't match the source byte-for-byte.- CSS variables
--atomic-editor-initial-reveal-bgand--atomic-editor-initial-reveal-bg-strongfor theming the peak and settled colors of the reveal highlight independently of the main search-match palette.
- Click routing after block widgets. Clicks on lines below a table
would route the caret to the line below the one visually targeted —
most visible as "clicking the blank line above a heading placed the
caret on the heading". Root cause:
.cm-atomic-tableused verticalmarginfor rhythm, whichgetBoundingClientRect(CM6's widget measurement) excludes but DOM layout reserves. The heightmap ran ~17 px short of reality for every line below the table. Changed topadding, which CM6 measures correctly.
- Shrink heading
padding-topso the visually-empty strip above a heading is ~3 px instead of ~14 px — reduces the separate class of "clicked above the heading, landed on it" UX cases. - Demo homepage now leads with the hero trio (code block, table, task list) and uses "Atomic Editor" as the display name in the header and tab title.
Extracted from Atomic as a standalone package.
AtomicCodeMirrorEditorReact component with Obsidian-style inline live preview: stable layout across active / inactive lines, no reveal-during-click, tight-list continuation, pointer-freeze guard on mouse interaction.- Interactive WYSIWYG table widget (in-place cell editing, click-to- rebuild, horizontal scroll for wide tables).
- Image block rendering (inline
source hidden below a rendered image with keep-size placeholder). - Dark-theme defaults +
[data-theme="light"]light variant via CSS variables only — no JavaScript toggle needed. - Syntax highlighting for fenced code blocks via the
codeLanguagesprop. An optional curated 20-language registry is exported at@atomic-editor/editor/code-languageswith lazy-loaded grammars. - Minimal search panel (input + match counter + prev/next/close), styled to match the editor theme.