@pyreon/code
Reactive code editor built on CodeMirror 6. Signal-backed state, lazy-loaded languages, custom minimap, diff editor, tabbed multi-file editing. ~250KB modular instead of Monaco's ~2.5MB.
Installation
bun add @pyreon/codePeer dependencies: @pyreon/core, @pyreon/reactivity
Quick Start
import { createEditor, CodeEditor } from '@pyreon/code'
const editor = createEditor({
value: 'const greeting = "Hello, Pyreon!"',
language: 'typescript',
theme: 'dark',
})
<CodeEditor instance={editor} style="height: 400px" />Signal-Backed State
Every piece of editor state is a reactive signal:
// Read reactively
editor.value() // current content
editor.language() // current language
editor.theme() // current theme
editor.readOnly() // read-only state
editor.cursor() // { line: number, col: number }
editor.selection() // { from: number, to: number, text: string }
editor.lineCount() // number of lines
editor.focused() // has focus
// Write — editor updates automatically
editor.value.set('new content')
editor.language.set('python')
editor.theme.set('dark')
editor.readOnly.set(true)Configuration
const editor = createEditor({
value: '', // initial content
language: 'typescript', // syntax highlighting language
theme: 'dark', // 'light' | 'dark' | custom Extension
lineNumbers: true, // show line numbers
readOnly: false, // read-only mode
foldGutter: true, // code folding
bracketMatching: true, // bracket matching + auto-close
autocomplete: true, // code completion
search: true, // find & replace (Cmd+F)
tabSize: 2, // tab width
lineWrapping: false, // wrap long lines
highlightIndentGuides: true, // indent guide lines
placeholder: 'Type here...', // placeholder when empty
minimap: true, // code overview sidebar
vim: false, // vim keybinding mode
emacs: false, // emacs keybinding mode
extensions: [], // additional CodeMirror extensions
onChange: (value) => {}, // called on content change
})Languages
20+ languages, lazy-loaded on demand — zero cost until used:
editor.language.set('typescript') // switch language dynamicallySupported: javascript, typescript, jsx, tsx, html, css, json, markdown, python, rust, sql, xml, yaml, cpp, java, go, php, ruby, shell, plain
import { getAvailableLanguages, loadLanguage } from '@pyreon/code'
getAvailableLanguages() // list all supported
await loadLanguage('typescript') // preload a languageThemes
import { lightTheme, darkTheme, resolveTheme } from '@pyreon/code'
// Switch dynamically
editor.theme.set('dark')
editor.theme.set('light')
// Custom theme — pass any CodeMirror theme Extension
editor.theme.set(myCustomTheme)Actions
editor.focus() // focus the editor
editor.insert('// comment') // insert at cursor
editor.replaceSelection('replacement') // replace selected text
editor.select(0, 10) // select range
editor.selectAll() // select all
editor.goToLine(42) // jump to line
editor.undo() // undo
editor.redo() // redo
editor.foldAll() // fold all code blocks
editor.unfoldAll() // unfold all
editor.scrollTo(position) // scroll to character position
insert/replaceSelectionare cursor-relative — they need a mounted view.The CodeMirror view is created bymount()after an async grammar load, so callingeditor.insert(...)/editor.replaceSelection(...)before the editor has mounted has no cursor to act on — the call is dropped (with a dev-mode warning). To set content independently of the view (before mount, or from a signal/CRDT binding), useeditor.value.set(...)— it feeds the value signal, which seeds the document whenever the view is created.
Diagnostics (Lint Integration)
Push diagnostics from external tools (TypeScript, ESLint, etc.):
editor.setDiagnostics([
{ from: 0, to: 5, severity: 'error', message: 'Unexpected token', source: 'typescript' },
{ from: 20, to: 30, severity: 'warning', message: 'Unused variable', source: 'eslint' },
])
editor.clearDiagnostics()Severities: 'error' | 'warning' | 'info' | 'hint'
Line Highlights
Highlight specific lines (errors, breakpoints, current execution):
editor.highlightLine(5, 'error-line') // add highlight
editor.highlightLine(10, 'current-line') // different style
editor.clearLineHighlights() // remove allGutter Markers
Add icons in the gutter (breakpoints, error indicators):
editor.setGutterMarker(5, { text: '🔴', title: 'Breakpoint' })
editor.setGutterMarker(12, { text: '⚠️', title: 'Warning', class: 'warning-marker' })
editor.clearGutterMarkers()Custom Keybindings
editor.addKeybinding('Ctrl-Shift-L', () => {
console.log('Custom shortcut!')
return true
})Text Queries
editor.getLine(5) // text of line 5
editor.getWordAtCursor() // word under cursorMinimap
Canvas-based code overview with viewport indicator and click-to-scroll:
const editor = createEditor({
value: longCode,
minimap: true, // enable minimap
})The minimap renders a scaled-down view of the entire document on the right side. Click to jump to that section. The viewport rectangle shows your current position.
Diff Editor
Side-by-side or inline diff using @codemirror/merge:
import { DiffEditor } from '@pyreon/code'
<DiffEditor
original="const x = 1\nconst y = 2"
modified="const x = 1\nconst y = 3\nconst z = 4"
language="typescript"
theme="dark"
style="height: 400px"
/>
// Inline diff
<DiffEditor original={old} modified={new} inline />
// Reactive — pass signals
<DiffEditor original={originalSignal} modified={modifiedSignal} />Tabbed Editor
Multi-file editing with tab management:
import { createTabbedEditor, TabbedEditor } from '@pyreon/code'
const editor = createTabbedEditor({
tabs: [
{ name: 'index.ts', language: 'typescript', value: 'const x = 1' },
{ name: 'style.css', language: 'css', value: '.app { color: red; }' },
{ name: 'data.json', language: 'json', value: '{ "key": "value" }' },
],
theme: 'dark',
})
<TabbedEditor instance={editor} style="height: 500px" />Tab Operations
editor.tabs() // Signal<Tab[]> — all open tabs
editor.activeTab() // Computed<Tab | null> — current tab
editor.activeTabId() // Signal<string>
// Lifecycle
editor.openTab({ name: 'utils.ts', language: 'typescript', value: '' })
editor.closeTab('style.css')
editor.switchTab('index.ts')
// Management
editor.renameTab('index.ts', 'main.ts')
editor.setModified('index.ts', true) // show modified indicator
editor.moveTab(0, 2) // reorder
editor.closeAll() // close all closable tabs
editor.closeOthers('index.ts') // close all except one
editor.getTab('index.ts') // get tab by idTab Features
Modified indicator — dot shown on tabs with unsaved changes
Closable tabs — set
closable: falsefor pinned tabsContent preservation — content cached when switching tabs
Auto-switch — closing active tab switches to adjacent
Vim / Emacs Mode
Optional key modes (requires installing the package):
bun add @replit/codemirror-vim # for vim mode
bun add @replit/codemirror-emacs # for emacs modeconst editor = createEditor({
value: 'hello world',
vim: true, // enable vim mode
})Accessing CodeMirror Directly
For advanced use cases, access the underlying EditorView:
const view = editor.view() // EditorView | null (null before mount)
if (view) {
// Use any CodeMirror API directly
view.dispatch({ ... })
}API Reference
createEditor
| Property | Type | Description |
|---|---|---|
value | Signal<string> | Editor content — reactive |
language | Signal<EditorLanguage> | Current language |
theme | Signal<EditorTheme> | Current theme |
readOnly | Signal<boolean> | Read-only state |
cursor | Computed<{line, col}> | Cursor position |
selection | Computed<{from, to, text}> | Current selection |
lineCount | Computed<number> | Number of lines |
focused | Signal<boolean> | Focus state |
view | Signal<EditorView | null> | CodeMirror instance |
createTabbedEditor
| Method | Description |
|---|---|
openTab(tab) | Open or switch to a tab |
closeTab(id) | Close a tab |
switchTab(id) | Switch to a tab |
renameTab(id, name) | Rename a tab |
setModified(id, bool) | Mark modified |
moveTab(from, to) | Reorder tabs |
closeAll() | Close all closable tabs |
closeOthers(id) | Close all except one |
Components
| Component | Description |
|---|---|
<CodeEditor> | Single-file editor |
<DiffEditor> | Side-by-side or inline diff |
<TabbedEditor> | Multi-file with tab bar |