pyreon

@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/code

Peer 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 dynamically

Supported: 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 language

Themes

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 / replaceSelection are cursor-relative — they need a mounted view.The CodeMirror view is created by mount() after an async grammar load, so calling editor.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), use editor.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 all

Gutter 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 cursor

Minimap

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 id

Tab Features

  • Modified indicator — dot shown on tabs with unsaved changes

  • Closable tabs — set closable: false for pinned tabs

  • Content 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 mode
const 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

PropertyTypeDescription
valueSignal<string>Editor content — reactive
languageSignal<EditorLanguage>Current language
themeSignal<EditorTheme>Current theme
readOnlySignal<boolean>Read-only state
cursorComputed<&#123;line, col&#125;>Cursor position
selectionComputed<&#123;from, to, text&#125;>Current selection
lineCountComputed<number>Number of lines
focusedSignal<boolean>Focus state
viewSignal<EditorView | null>CodeMirror instance

createTabbedEditor

MethodDescription
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

ComponentDescription
<CodeEditor>Single-file editor
<DiffEditor>Side-by-side or inline diff
<TabbedEditor>Multi-file with tab bar
Code Editor