@atomic-editor/editor

中文文档
0.6.2发布于 2026/07/12MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 2.1万

CodeMirror 6 markdown editor with Obsidian-style inline live preview.

原始使用文档
文档目录16 节
  1. Features
  2. Install
  3. Use
  4. Imperative handle
  5. Read-only (reading) mode
  6. Arriving from a search result
  7. Syntax highlighting
  8. Option 1: use the curated list (~20 languages)
  9. Option 2: bring your own
  10. Wiki links
  11. Theming
  12. Extending with plugins
  13. Low-level composition
  14. Design notes
  15. Contributing
  16. License

Atomic Editor

Obsidian-style live preview for CodeMirror 6, in React.

npm version license

A markdown editor where formatting renders as you type — headings, bold, tables, images, task lists — while the text underneath stays plain markdown. The document you read is the document you edit: no split preview, and copy / save / round-trip behave exactly like a plain textarea full of markdown. An optional reading mode locks that same rendered surface without introducing a separate preview document.

It's the writing surface behind Atomic, a personal knowledge base — extracted to stand on its own, and hardened on real user documents.

Try the live demo →

Features

  • Live preview. Headings, emphasis, ==highlights==, links, images, and tables render inline; the raw syntax appears only on the line your cursor is on, then tucks itself away when you move on.
  • Raw markdown is the source of truth. Every decoration is view-only, so copy, save, and round-trip through any other markdown tool are byte-for-byte identical to a plain textarea.
  • Virtualized and layout-stable. CM6 renders only the viewport, and lines never reflow when you click into them — open a 500-page document and scroll stays smooth, even on iOS.
  • WYSIWYG tables. Click a cell to edit in place; wide tables scroll horizontally inside a contained wrapper instead of stretching the page.
  • Wiki links. [[target]] / [[target|label]] with async resolution, autocomplete, and click-to-open — for knowledge-base-style cross-linking.
  • Smart lists. Enter continues tight bullets and task checkboxes, Enter on an empty item dedents, and - [ ] becomes a real, clickable checkbox.
  • Syntax-highlighted code for 20+ languages, each grammar lazy-loaded the first time a fence uses it so unused languages never hit the wire.
  • Themed with CSS variables — dark by default, light via a single data-theme="light" attribute, every color overridable.
  • Minimal find panel (Ctrl/Cmd+F) styled to match the editor.

Install

npm install @atomic-editor/editor \
  @codemirror/state @codemirror/view @codemirror/commands \
  @codemirror/autocomplete @codemirror/language @codemirror/search \
  @codemirror/lang-markdown \
  @lezer/common @lezer/highlight @lezer/markdown \
  react react-dom

The CodeMirror and React packages are declared as peer dependencies rather than regular deps. You install them alongside the editor so your bundler resolves a single shared copy — two copies of @codemirror/state in one bundle would silently break the editor's state-field identity checks.

Fenced-code language grammars (@codemirror/lang-javascript, @codemirror/lang-python, etc.) are optional peers — install only the ones you want highlighted. See Syntax highlighting below.

Use

import { AtomicCodeMirrorEditor } from '@atomic-editor/editor';
import '@atomic-editor/editor/styles.css';

function App() {
  return (
    <AtomicCodeMirrorEditor
      markdownSource={'# Hello\n\nA paragraph.'}
      onMarkdownChange={(md) => console.log(md)}
      onLinkClick={(url) => window.open(url, '_blank', 'noopener,noreferrer')}
    />
  );
}

The editor fills its parent — wrap it in a height-bounded flex or grid container.

Imperative handle

Pass a ref if you need to drive the editor from outside — e.g. wire your own toolbar buttons, or open the search panel from a global keybinding:

import { useRef } from 'react';
import {
  AtomicCodeMirrorEditor,
  type AtomicCodeMirrorEditorHandle,
} from '@atomic-editor/editor';

function App() {
  const editor = useRef<AtomicCodeMirrorEditorHandle | null>(null);
  return (
    <>
      <button onClick={() => editor.current?.openSearch()}>Search</button>
      <AtomicCodeMirrorEditor
        markdownSource={'…'}
        editorHandleRef={editor}
      />
    </>
  );
}

Methods: focus, undo, redo, openSearch(query?), closeSearch, revealText(query), isSearchOpen, getMarkdown, getContentDOM, setReadOnly(readOnly).

Read-only (reading) mode

Pass readOnly to render the document as a reading surface, like Obsidian's Reading view:

<AtomicCodeMirrorEditor markdownSource={'…'} readOnly />

In read-only mode the whole document stays rendered — source never reveals under a caret — typing / paste / table editing are disabled, and clicking a link (anywhere on it, not just the trailing icon) opens it instead of placing a caret. Task checkboxes stay toggleable and find-in-document still works.

readOnly is backed by a CodeMirror Compartment, so flipping it reconfigures the live view in place — scroll position and search state are preserved, no remount. Drive it from a prop, or imperatively via editorHandle.setReadOnly(true) for a toolbar toggle outside React's render cycle. For consumers composing a custom editor, the underlying readOnlyFacet and readOnlyExtension are exported too.

Arriving from a search result

Two props drop the user near a relevant paragraph on mount:

  • initialSearchText opens the search panel pre-filled with the query. Full navigation surface — arrow keys to step through matches, close to dismiss. Good when the user explicitly invoked find.
  • initialRevealText does a less intrusive scroll-into-view with a 3.2 s fade-out highlight on the first match — no panel, no cursor move. Good for "I clicked a search result, take me to the paragraph it came from".

Both accept string | null. The reveal matcher falls back progressively — exact, whitespace-collapsed, individual lines, then truncated prefixes (140 and 80 chars) — so hits still resolve when the query came from an LLM-massaged snippet that doesn't match the source byte-for-byte. For post-mount reveals, call editorHandle.revealText(query) via the imperative handle.

The fade highlight uses CSS variables --atomic-editor-initial-reveal-bg and --atomic-editor-initial-reveal-bg-strong; override to theme the peak and settled colors independently of the main search-match palette.

Syntax highlighting

Fenced code blocks are plain monospace by default. To enable highlighting, pass a codeLanguages array. @codemirror/lang-markdown dynamically imports each grammar the first time a fence uses it, so large lists don't bloat the initial bundle.

Option 1: use the curated list (~20 languages)

# Install the lang-* peers you want highlighted.
npm install \
  @codemirror/lang-javascript @codemirror/lang-python \
  @codemirror/lang-rust @codemirror/lang-go @codemirror/lang-html \
  @codemirror/lang-css @codemirror/lang-json @codemirror/lang-yaml \
  @codemirror/legacy-modes  # ruby/swift/shell/toml/dockerfile
import { AtomicCodeMirrorEditor } from '@atomic-editor/editor';
import { ATOMIC_CODE_LANGUAGES } from '@atomic-editor/editor/code-languages';

<AtomicCodeMirrorEditor
  markdownSource={'…'}
  codeLanguages={ATOMIC_CODE_LANGUAGES}
/>

See src/code-languages.ts for the full list (JavaScript, TypeScript, Python, Go, Rust, Ruby, Java, C, C++, PHP, Swift, Shell, SQL, HTML, CSS, XML, JSON, YAML, TOML, Dockerfile, Markdown).

Option 2: bring your own

import { LanguageDescription } from '@codemirror/language';
import { python } from '@codemirror/lang-python';

const codeLanguages = [
  LanguageDescription.of({
    name: 'Python',
    alias: ['py'],
    extensions: ['py'],
    load: () => Promise.resolve(python()),
  }),
];

<AtomicCodeMirrorEditor markdownSource={'…'} codeLanguages={codeLanguages} />

[[target]] and [[target|label]] links — the way Atomic and Obsidian cross-link notes — ship as a composable extension. It renders labeled links, resolves bare targets asynchronously (to show a real title and a resolved / missing state), opens links on click, and offers autocomplete as soon as you type [[:

import { AtomicCodeMirrorEditor, wikiLinks } from '@atomic-editor/editor';

<AtomicCodeMirrorEditor
  markdownSource={'See [[atom-42|the design doc]] for details.'}
  extensions={[
    wikiLinks({
      suggest: async (query) => store.search(query),     // autocomplete source
      resolve: async (target) => store.resolve(target),  // label + status for bare links
      onOpen: (target) => router.open(target),           // click / Cmd-click to navigate
    }),
  ]}
/>;

Draft links stay editable while the cursor is inside them; resolution is debounced and cached. See src/wiki-links.ts for the full config — custom serialization, resolver policies, suggestion limits, and the WikiLinkSuggestion / WikiLinkResolvedTarget types.

Theming

Every color, font, and size reads from a CSS custom property with an inline fallback. Override on any ancestor of the editor.

The package ships a light variant that activates whenever data-theme="light" is set on an ancestor — including <html> or <body>. The dark defaults remain unchanged; the light block just re-maps the same variables.

<html data-theme="light">…</html>
VariableDark default (auto-light on [data-theme="light"])
--atomic-editor-fontsystem sans
--atomic-editor-font-monosystem mono
--atomic-editor-body-size1.0625rem
--atomic-editor-body-leading1.7
--atomic-editor-measure70ch
--atomic-editor-fg#dcddde
--atomic-editor-fg-muted#888
--atomic-editor-fg-faint#666
--atomic-editor-bg#1e1e1e
--atomic-editor-bg-panel#252525
--atomic-editor-bg-surface#2d2d2d
--atomic-editor-border#3d3d3d
--atomic-editor-accent#7c3aed
--atomic-editor-accent-bright#a78bfa
--atomic-editor-accent-softblockquote rail / reveal tint
--atomic-editor-link#818cf8
--atomic-editor-link-hover#a5b4fc
--atomic-editor-code-bgsubtle dark panel
--atomic-editor-selection-bgaccent-tinted 28%
--atomic-editor-search-bgaccent-tinted 28%
--atomic-editor-search-bg-activeaccent-tinted 60%
Code-token colors (Palenight)
--atomic-editor-hl-keyword#c792ea
--atomic-editor-hl-string#c3e88d
--atomic-editor-hl-number#f78c6c
--atomic-editor-hl-comment#6a7a82
--atomic-editor-hl-type#ffcb6b
--atomic-editor-hl-function#82aaff
--atomic-editor-hl-property#82aaff
--atomic-editor-hl-regexp#f07178
--atomic-editor-hl-escape#89ddff
--atomic-editor-hl-tag#f07178
--atomic-editor-hl-variable#eeffff
--atomic-editor-hl-operator#89ddff
--atomic-editor-hl-invalid#ff5370

Extending with plugins

CodeMirror 6 is extension-based, and so is this package. Pass any number of CM6 extensions via the extensions prop to layer in autocomplete sources, custom decorations, domain-specific keymaps, collaboration (yjs), vim mode, or anything else. (The wiki-links extension above is built with exactly this hook.)

import { autocompletion, type CompletionContext } from '@codemirror/autocomplete';

const hashtags = autocompletion({
  override: [(ctx: CompletionContext) => {
    const match = ctx.matchBefore(/#\w*$/);
    if (!match) return null;
    return {
      from: match.from + 1,
      options: myTagStore.list().map((tag) => ({ label: tag })),
    };
  }],
});

<AtomicCodeMirrorEditor
  markdownSource={'…'}
  extensions={[hashtags]}
/>

Consumer extensions are appended after the built-ins, so wrap a custom keymap in Prec.high (from @codemirror/state) if it needs to beat the default bindings. The array is captured at mount — pass a stable reference unless you want a remount.

Low-level composition

If the React wrapper's extension set is too opinionated, every piece is exported individually so you can assemble a fully custom editor:

import {
  inlinePreview, // live preview decorations
  imageBlocks,   // rendered image widgets
  tables,        // WYSIWYG table widget
  wikiLinks,     // [[...]] links
  atomicEditorTheme,
  atomicMarkdownSyntax,
  extendEmphasisPair,
} from '@atomic-editor/editor';

You could build an editor that includes inlinePreview() + tables() but skips atomicEditorTheme for your own EditorView.theme({...}), or swap atomicMarkdownSyntax for a custom syntaxHighlighting(HighlightStyle.define([...])). At that point you're outside the React wrapper and in plain CM6 territory.

Design notes

See docs/architecture.md for the full design rationale. Short version:

  • Raw markdown is the source of truth. All decorations are view-only — copy, save, and round-trip to any markdown parser are identical to what you'd expect from a plain textarea.
  • No layout shifts. Every line has a stable height regardless of cursor position. Inline decorations hide syntax tokens on inactive lines without changing line heights.
  • Narrow invalidation. Decoration rebuilds only touch lines whose content (or surrounding trigger characters) changed, so editing a paragraph in a 50KB doc costs O(change size), not O(doc).
  • Mouse-freeze guard. Clicks don't trigger a decoration rebuild mid-interaction — eliminates a class of cursor-drift bugs.
  • iOS-aware. Momentum-scroll halts (image remount jank, heightmap drift, anchor conflicts) were tracked down and fixed; the demo's sample-size picker doubles as a stress harness for spotting any regressions.

Contributing

Development requires Node.js 20.19+, 22.12+, or 24+. The published editor itself continues to support Node.js 18 and newer.

git clone https://github.com/kenforthewin/atomic-editor
cd atomic-editor
npm install
npm run dev        # demo dev server at http://localhost:5173
npm test           # vitest unit tests
npm run build      # tsc emit to dist/
npm run test:e2e   # legacy probes + deterministic browser suites
npm run test:package  # pack and build a clean consumer app

The browser harness combines the broad legacy probe suite with focused, isolated Playwright Test specs. Chromium runs the full matrix; Firefox and WebKit run the compatibility smoke tests. See docs/testing.md for the layers, commands, and the rule for turning a bug fix into a lasting regression test.

Because the editor ships inside Atomic, real user documents are its de-facto fuzz corpus — odd inputs (multi-line link titles, over-escaped RSS imports, wide tables) tend to surface there first, and fixes land here. Issues and PRs welcome.

License

MIT. See LICENSE.

版本与发布记录

@atomic-editor/editor 共发布 14 个版本,首次发布于 2026/04/23;过去 12 个月发布了 14 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.6.2latest2026/07/123 个月前
0.6.12026/07/123 个月前
0.6.02026/07/123 个月前
0.5.12026/07/123 个月前
0.5.02026/07/123 个月前
0.4.32026/06/015 个月前

关于 @atomic-editor/editor 的常见问题

@atomic-editor/editor 是什么?

CodeMirror 6 markdown editor with Obsidian-style inline live preview.

如何安装 @atomic-editor/editor?

在项目目录运行 npm install @atomic-editor/editor,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@codemirror/autocomplete(^6.0.0)、@codemirror/commands(^6.0.0)、@codemirror/lang-markdown(^6.0.0)、@codemirror/language(^6.0.0)、@codemirror/search(^6.0.0)、@codemirror/state(^6.0.0)、@codemirror/view(^6.0.0)、@lezer/common(^1.0.0)、@lezer/highlight(^1.0.0)、@lezer/markdown(^1.0.0)、react(^18.0.0 || ^19.0.0)、react-dom(^18.0.0 || ^19.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@atomic-editor/editor 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@atomic-editor/editor 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@atomic-editor/editor 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@atomic-editor/editor 可以商用吗?

@atomic-editor/editor 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@atomic-editor/editor 的下载量有多少?

根据 npm 官方下载统计,@atomic-editor/editor 在 2026/10/07 – 2026/11/05 这 30 天内被下载 20,927 次,日均约 698 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@atomic-editor/editor 还在维护吗?

最新版本 0.6.2 发布于 2026/07/12(3 个月前);至今共发布 14 个版本,其中过去 12 个月发布了 14 个;首次发布于 2026/04/23。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。