|
| 1 | +--- |
| 2 | +title: Code Editor |
| 3 | +description: Reactive code editor for Pyreon — CodeMirror 6 with signals, minimap, diff editor, tabs, lazy-loaded languages |
| 4 | +--- |
| 5 | + |
| 6 | +# @pyreon/code |
| 7 | + |
| 8 | +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. |
| 9 | + |
| 10 | +## Installation |
| 11 | + |
| 12 | +```bash |
| 13 | +bun add @pyreon/code |
| 14 | +``` |
| 15 | + |
| 16 | +Peer dependencies: `@pyreon/core`, `@pyreon/reactivity` |
| 17 | + |
| 18 | +## Quick Start |
| 19 | + |
| 20 | +```tsx |
| 21 | +import { createEditor, CodeEditor } from '@pyreon/code' |
| 22 | + |
| 23 | +const editor = createEditor({ |
| 24 | + value: 'const greeting = "Hello, Pyreon!"', |
| 25 | + language: 'typescript', |
| 26 | + theme: 'dark', |
| 27 | +}) |
| 28 | + |
| 29 | +<CodeEditor instance={editor} style="height: 400px" /> |
| 30 | +``` |
| 31 | + |
| 32 | +## Signal-Backed State |
| 33 | + |
| 34 | +Every piece of editor state is a reactive signal: |
| 35 | + |
| 36 | +```tsx |
| 37 | +// Read reactively |
| 38 | +editor.value() // current content |
| 39 | +editor.language() // current language |
| 40 | +editor.theme() // current theme |
| 41 | +editor.readOnly() // read-only state |
| 42 | +editor.cursor() // { line: number, col: number } |
| 43 | +editor.selection() // { from: number, to: number, text: string } |
| 44 | +editor.lineCount() // number of lines |
| 45 | +editor.focused() // has focus |
| 46 | + |
| 47 | +// Write — editor updates automatically |
| 48 | +editor.value.set('new content') |
| 49 | +editor.language.set('python') |
| 50 | +editor.theme.set('dark') |
| 51 | +editor.readOnly.set(true) |
| 52 | +``` |
| 53 | + |
| 54 | +## Configuration |
| 55 | + |
| 56 | +```tsx |
| 57 | +const editor = createEditor({ |
| 58 | + value: '', // initial content |
| 59 | + language: 'typescript', // syntax highlighting language |
| 60 | + theme: 'dark', // 'light' | 'dark' | custom Extension |
| 61 | + lineNumbers: true, // show line numbers |
| 62 | + readOnly: false, // read-only mode |
| 63 | + foldGutter: true, // code folding |
| 64 | + bracketMatching: true, // bracket matching + auto-close |
| 65 | + autocomplete: true, // code completion |
| 66 | + search: true, // find & replace (Cmd+F) |
| 67 | + tabSize: 2, // tab width |
| 68 | + lineWrapping: false, // wrap long lines |
| 69 | + highlightIndentGuides: true, // indent guide lines |
| 70 | + placeholder: 'Type here...', // placeholder when empty |
| 71 | + minimap: true, // code overview sidebar |
| 72 | + vim: false, // vim keybinding mode |
| 73 | + emacs: false, // emacs keybinding mode |
| 74 | + extensions: [], // additional CodeMirror extensions |
| 75 | + onChange: (value) => {}, // called on content change |
| 76 | +}) |
| 77 | +``` |
| 78 | + |
| 79 | +## Languages |
| 80 | + |
| 81 | +20+ languages, lazy-loaded on demand — zero cost until used: |
| 82 | + |
| 83 | +```tsx |
| 84 | +editor.language.set('typescript') // switch language dynamically |
| 85 | +``` |
| 86 | + |
| 87 | +Supported: `javascript`, `typescript`, `jsx`, `tsx`, `html`, `css`, `json`, `markdown`, `python`, `rust`, `sql`, `xml`, `yaml`, `cpp`, `java`, `go`, `php`, `ruby`, `shell`, `plain` |
| 88 | + |
| 89 | +```tsx |
| 90 | +import { getAvailableLanguages, loadLanguage } from '@pyreon/code' |
| 91 | + |
| 92 | +getAvailableLanguages() // list all supported |
| 93 | +await loadLanguage('typescript') // preload a language |
| 94 | +``` |
| 95 | + |
| 96 | +## Themes |
| 97 | + |
| 98 | +```tsx |
| 99 | +import { lightTheme, darkTheme, resolveTheme } from '@pyreon/code' |
| 100 | + |
| 101 | +// Switch dynamically |
| 102 | +editor.theme.set('dark') |
| 103 | +editor.theme.set('light') |
| 104 | + |
| 105 | +// Custom theme — pass any CodeMirror theme Extension |
| 106 | +editor.theme.set(myCustomTheme) |
| 107 | +``` |
| 108 | + |
| 109 | +## Actions |
| 110 | + |
| 111 | +```tsx |
| 112 | +editor.focus() // focus the editor |
| 113 | +editor.insert('// comment') // insert at cursor |
| 114 | +editor.replaceSelection('replacement') // replace selected text |
| 115 | +editor.select(0, 10) // select range |
| 116 | +editor.selectAll() // select all |
| 117 | +editor.goToLine(42) // jump to line |
| 118 | +editor.undo() // undo |
| 119 | +editor.redo() // redo |
| 120 | +editor.foldAll() // fold all code blocks |
| 121 | +editor.unfoldAll() // unfold all |
| 122 | +editor.scrollTo(position) // scroll to character position |
| 123 | +``` |
| 124 | + |
| 125 | +## Diagnostics (Lint Integration) |
| 126 | + |
| 127 | +Push diagnostics from external tools (TypeScript, ESLint, etc.): |
| 128 | + |
| 129 | +```tsx |
| 130 | +editor.setDiagnostics([ |
| 131 | + { from: 0, to: 5, severity: 'error', message: 'Unexpected token', source: 'typescript' }, |
| 132 | + { from: 20, to: 30, severity: 'warning', message: 'Unused variable', source: 'eslint' }, |
| 133 | +]) |
| 134 | + |
| 135 | +editor.clearDiagnostics() |
| 136 | +``` |
| 137 | + |
| 138 | +Severities: `'error'` | `'warning'` | `'info'` | `'hint'` |
| 139 | + |
| 140 | +## Line Highlights |
| 141 | + |
| 142 | +Highlight specific lines (errors, breakpoints, current execution): |
| 143 | + |
| 144 | +```tsx |
| 145 | +editor.highlightLine(5, 'error-line') // add highlight |
| 146 | +editor.highlightLine(10, 'current-line') // different style |
| 147 | +editor.clearLineHighlights() // remove all |
| 148 | +``` |
| 149 | + |
| 150 | +## Gutter Markers |
| 151 | + |
| 152 | +Add icons in the gutter (breakpoints, error indicators): |
| 153 | + |
| 154 | +```tsx |
| 155 | +editor.setGutterMarker(5, { text: '🔴', title: 'Breakpoint' }) |
| 156 | +editor.setGutterMarker(12, { text: '⚠️', title: 'Warning', class: 'warning-marker' }) |
| 157 | +editor.clearGutterMarkers() |
| 158 | +``` |
| 159 | + |
| 160 | +## Custom Keybindings |
| 161 | + |
| 162 | +```tsx |
| 163 | +editor.addKeybinding('Ctrl-Shift-L', () => { |
| 164 | + console.log('Custom shortcut!') |
| 165 | + return true |
| 166 | +}) |
| 167 | +``` |
| 168 | + |
| 169 | +## Text Queries |
| 170 | + |
| 171 | +```tsx |
| 172 | +editor.getLine(5) // text of line 5 |
| 173 | +editor.getWordAtCursor() // word under cursor |
| 174 | +``` |
| 175 | + |
| 176 | +## Minimap |
| 177 | + |
| 178 | +Canvas-based code overview with viewport indicator and click-to-scroll: |
| 179 | + |
| 180 | +```tsx |
| 181 | +const editor = createEditor({ |
| 182 | + value: longCode, |
| 183 | + minimap: true, // enable minimap |
| 184 | +}) |
| 185 | +``` |
| 186 | + |
| 187 | +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. |
| 188 | + |
| 189 | +## Diff Editor |
| 190 | + |
| 191 | +Side-by-side or inline diff using `@codemirror/merge`: |
| 192 | + |
| 193 | +```tsx |
| 194 | +import { DiffEditor } from '@pyreon/code' |
| 195 | + |
| 196 | +<DiffEditor |
| 197 | + original="const x = 1\nconst y = 2" |
| 198 | + modified="const x = 1\nconst y = 3\nconst z = 4" |
| 199 | + language="typescript" |
| 200 | + theme="dark" |
| 201 | + style="height: 400px" |
| 202 | +/> |
| 203 | + |
| 204 | +// Inline diff |
| 205 | +<DiffEditor original={old} modified={new} inline /> |
| 206 | + |
| 207 | +// Reactive — pass signals |
| 208 | +<DiffEditor original={originalSignal} modified={modifiedSignal} /> |
| 209 | +``` |
| 210 | + |
| 211 | +## Tabbed Editor |
| 212 | + |
| 213 | +Multi-file editing with tab management: |
| 214 | + |
| 215 | +```tsx |
| 216 | +import { createTabbedEditor, TabbedEditor } from '@pyreon/code' |
| 217 | + |
| 218 | +const editor = createTabbedEditor({ |
| 219 | + tabs: [ |
| 220 | + { name: 'index.ts', language: 'typescript', value: 'const x = 1' }, |
| 221 | + { name: 'style.css', language: 'css', value: '.app { color: red; }' }, |
| 222 | + { name: 'data.json', language: 'json', value: '{ "key": "value" }' }, |
| 223 | + ], |
| 224 | + theme: 'dark', |
| 225 | +}) |
| 226 | + |
| 227 | +<TabbedEditor instance={editor} style="height: 500px" /> |
| 228 | +``` |
| 229 | + |
| 230 | +### Tab Operations |
| 231 | + |
| 232 | +```tsx |
| 233 | +editor.tabs() // Signal<Tab[]> — all open tabs |
| 234 | +editor.activeTab() // Computed<Tab | null> — current tab |
| 235 | +editor.activeTabId() // Signal<string> |
| 236 | + |
| 237 | +// Lifecycle |
| 238 | +editor.openTab({ name: 'utils.ts', language: 'typescript', value: '' }) |
| 239 | +editor.closeTab('style.css') |
| 240 | +editor.switchTab('index.ts') |
| 241 | + |
| 242 | +// Management |
| 243 | +editor.renameTab('index.ts', 'main.ts') |
| 244 | +editor.setModified('index.ts', true) // show modified indicator |
| 245 | +editor.moveTab(0, 2) // reorder |
| 246 | +editor.closeAll() // close all closable tabs |
| 247 | +editor.closeOthers('index.ts') // close all except one |
| 248 | +editor.getTab('index.ts') // get tab by id |
| 249 | +``` |
| 250 | + |
| 251 | +### Tab Features |
| 252 | + |
| 253 | +- **Modified indicator** — dot shown on tabs with unsaved changes |
| 254 | +- **Closable tabs** — set `closable: false` for pinned tabs |
| 255 | +- **Content preservation** — content cached when switching tabs |
| 256 | +- **Auto-switch** — closing active tab switches to adjacent |
| 257 | + |
| 258 | +## Vim / Emacs Mode |
| 259 | + |
| 260 | +Optional key modes (requires installing the package): |
| 261 | + |
| 262 | +```bash |
| 263 | +bun add @replit/codemirror-vim # for vim mode |
| 264 | +bun add @replit/codemirror-emacs # for emacs mode |
| 265 | +``` |
| 266 | + |
| 267 | +```tsx |
| 268 | +const editor = createEditor({ |
| 269 | + value: 'hello world', |
| 270 | + vim: true, // enable vim mode |
| 271 | +}) |
| 272 | +``` |
| 273 | + |
| 274 | +## Accessing CodeMirror Directly |
| 275 | + |
| 276 | +For advanced use cases, access the underlying EditorView: |
| 277 | + |
| 278 | +```tsx |
| 279 | +const view = editor.view() // EditorView | null (null before mount) |
| 280 | + |
| 281 | +if (view) { |
| 282 | + // Use any CodeMirror API directly |
| 283 | + view.dispatch({ ... }) |
| 284 | +} |
| 285 | +``` |
| 286 | + |
| 287 | +## API Reference |
| 288 | + |
| 289 | +### createEditor |
| 290 | + |
| 291 | +| Property | Type | Description | |
| 292 | +|---|---|---| |
| 293 | +| `value` | `Signal<string>` | Editor content — reactive | |
| 294 | +| `language` | `Signal<EditorLanguage>` | Current language | |
| 295 | +| `theme` | `Signal<EditorTheme>` | Current theme | |
| 296 | +| `readOnly` | `Signal<boolean>` | Read-only state | |
| 297 | +| `cursor` | `Computed<{line, col}>` | Cursor position | |
| 298 | +| `selection` | `Computed<{from, to, text}>` | Current selection | |
| 299 | +| `lineCount` | `Computed<number>` | Number of lines | |
| 300 | +| `focused` | `Signal<boolean>` | Focus state | |
| 301 | +| `view` | `Signal<EditorView \| null>` | CodeMirror instance | |
| 302 | + |
| 303 | +### createTabbedEditor |
| 304 | + |
| 305 | +| Method | Description | |
| 306 | +|---|---| |
| 307 | +| `openTab(tab)` | Open or switch to a tab | |
| 308 | +| `closeTab(id)` | Close a tab | |
| 309 | +| `switchTab(id)` | Switch to a tab | |
| 310 | +| `renameTab(id, name)` | Rename a tab | |
| 311 | +| `setModified(id, bool)` | Mark modified | |
| 312 | +| `moveTab(from, to)` | Reorder tabs | |
| 313 | +| `closeAll()` | Close all closable tabs | |
| 314 | +| `closeOthers(id)` | Close all except one | |
| 315 | + |
| 316 | +### Components |
| 317 | + |
| 318 | +| Component | Description | |
| 319 | +|---|---| |
| 320 | +| `<CodeEditor>` | Single-file editor | |
| 321 | +| `<DiffEditor>` | Side-by-side or inline diff | |
| 322 | +| `<TabbedEditor>` | Multi-file with tab bar | |
0 commit comments