@pyreon/code 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/lib/analysis/index.js.html +5406 -0
  3. package/lib/dist-B5vB-rif.js +3904 -0
  4. package/lib/dist-B5vB-rif.js.map +1 -0
  5. package/lib/dist-BAfzu5eu.js +1428 -0
  6. package/lib/dist-BAfzu5eu.js.map +1 -0
  7. package/lib/dist-BLlV_D16.js +1166 -0
  8. package/lib/dist-BLlV_D16.js.map +1 -0
  9. package/lib/dist-BNmKLTu8.js +373 -0
  10. package/lib/dist-BNmKLTu8.js.map +1 -0
  11. package/lib/dist-BZtTlC1J.js +692 -0
  12. package/lib/dist-BZtTlC1J.js.map +1 -0
  13. package/lib/dist-CTDqGIAf.js +856 -0
  14. package/lib/dist-CTDqGIAf.js.map +1 -0
  15. package/lib/dist-CTPisNZp.js +83 -0
  16. package/lib/dist-CTPisNZp.js.map +1 -0
  17. package/lib/dist-Ce2tvOxv.js +379 -0
  18. package/lib/dist-Ce2tvOxv.js.map +1 -0
  19. package/lib/dist-CttF0OTv.js +465 -0
  20. package/lib/dist-CttF0OTv.js.map +1 -0
  21. package/lib/dist-DS2tluW9.js +818 -0
  22. package/lib/dist-DS2tluW9.js.map +1 -0
  23. package/lib/dist-DUNx9ldu.js +460 -0
  24. package/lib/dist-DUNx9ldu.js.map +1 -0
  25. package/lib/dist-Dej_yf3k.js +473 -0
  26. package/lib/dist-Dej_yf3k.js.map +1 -0
  27. package/lib/dist-DshStUxU.js +283 -0
  28. package/lib/dist-DshStUxU.js.map +1 -0
  29. package/lib/dist-qTrOe7xY.js +461 -0
  30. package/lib/dist-qTrOe7xY.js.map +1 -0
  31. package/lib/dist-v09vikKr.js +2421 -0
  32. package/lib/dist-v09vikKr.js.map +1 -0
  33. package/lib/index.js +915 -0
  34. package/lib/index.js.map +1 -0
  35. package/lib/types/dist.d.ts +798 -0
  36. package/lib/types/dist.d.ts.map +1 -0
  37. package/lib/types/dist10.d.ts +67 -0
  38. package/lib/types/dist10.d.ts.map +1 -0
  39. package/lib/types/dist11.d.ts +126 -0
  40. package/lib/types/dist11.d.ts.map +1 -0
  41. package/lib/types/dist12.d.ts +21 -0
  42. package/lib/types/dist12.d.ts.map +1 -0
  43. package/lib/types/dist13.d.ts +404 -0
  44. package/lib/types/dist13.d.ts.map +1 -0
  45. package/lib/types/dist14.d.ts +292 -0
  46. package/lib/types/dist14.d.ts.map +1 -0
  47. package/lib/types/dist15.d.ts +132 -0
  48. package/lib/types/dist15.d.ts.map +1 -0
  49. package/lib/types/dist2.d.ts +15 -0
  50. package/lib/types/dist2.d.ts.map +1 -0
  51. package/lib/types/dist3.d.ts +106 -0
  52. package/lib/types/dist3.d.ts.map +1 -0
  53. package/lib/types/dist4.d.ts +67 -0
  54. package/lib/types/dist4.d.ts.map +1 -0
  55. package/lib/types/dist5.d.ts +95 -0
  56. package/lib/types/dist5.d.ts.map +1 -0
  57. package/lib/types/dist6.d.ts +330 -0
  58. package/lib/types/dist6.d.ts.map +1 -0
  59. package/lib/types/dist7.d.ts +15 -0
  60. package/lib/types/dist7.d.ts.map +1 -0
  61. package/lib/types/dist8.d.ts +15 -0
  62. package/lib/types/dist8.d.ts.map +1 -0
  63. package/lib/types/dist9.d.ts +635 -0
  64. package/lib/types/dist9.d.ts.map +1 -0
  65. package/lib/types/index.d.ts +852 -0
  66. package/lib/types/index.d.ts.map +1 -0
  67. package/lib/types/index2.d.ts +347 -0
  68. package/lib/types/index2.d.ts.map +1 -0
  69. package/package.json +79 -0
  70. package/src/components/code-editor.tsx +42 -0
  71. package/src/components/diff-editor.tsx +97 -0
  72. package/src/components/tabbed-editor.tsx +86 -0
  73. package/src/editor.ts +652 -0
  74. package/src/index.ts +52 -0
  75. package/src/languages.ts +77 -0
  76. package/src/minimap.ts +160 -0
  77. package/src/tabbed-editor.ts +231 -0
  78. package/src/tests/code.test.ts +505 -0
  79. package/src/themes.ts +87 -0
  80. package/src/types.ts +253 -0
package/src/editor.ts ADDED
@@ -0,0 +1,652 @@
1
+ import {
2
+ autocompletion,
3
+ closeBrackets,
4
+ closeBracketsKeymap,
5
+ completionKeymap,
6
+ } from '@codemirror/autocomplete'
7
+ import {
8
+ redo as cmRedo,
9
+ undo as cmUndo,
10
+ defaultKeymap,
11
+ history,
12
+ historyKeymap,
13
+ indentWithTab,
14
+ } from '@codemirror/commands'
15
+ import {
16
+ bracketMatching,
17
+ defaultHighlightStyle,
18
+ foldGutter,
19
+ foldKeymap,
20
+ indentOnInput,
21
+ indentUnit,
22
+ syntaxHighlighting,
23
+ } from '@codemirror/language'
24
+ import {
25
+ setDiagnostics as cmSetDiagnostics,
26
+ lintKeymap,
27
+ } from '@codemirror/lint'
28
+ import { highlightSelectionMatches, searchKeymap } from '@codemirror/search'
29
+ import { Compartment, EditorState, type Extension } from '@codemirror/state'
30
+ import {
31
+ GutterMarker as CMGutterMarker,
32
+ crosshairCursor,
33
+ Decoration,
34
+ type DecorationSet,
35
+ drawSelection,
36
+ dropCursor,
37
+ EditorView,
38
+ gutter,
39
+ highlightActiveLine,
40
+ highlightActiveLineGutter,
41
+ keymap,
42
+ lineNumbers,
43
+ placeholder as placeholderExt,
44
+ rectangularSelection,
45
+ ViewPlugin,
46
+ type ViewUpdate,
47
+ } from '@codemirror/view'
48
+ import { computed, effect, signal } from '@pyreon/reactivity'
49
+ import { loadLanguage } from './languages'
50
+ import { minimapExtension } from './minimap'
51
+ import { resolveTheme } from './themes'
52
+ import type {
53
+ EditorConfig,
54
+ EditorInstance,
55
+ EditorLanguage,
56
+ EditorTheme,
57
+ } from './types'
58
+
59
+ /**
60
+ * Create a reactive code editor instance.
61
+ *
62
+ * The editor state (value, language, theme, cursor, selection) is backed
63
+ * by signals. The CodeMirror EditorView is created when mounted via
64
+ * the `<CodeEditor>` component.
65
+ *
66
+ * @param config - Editor configuration
67
+ * @returns A reactive EditorInstance
68
+ *
69
+ * @example
70
+ * ```tsx
71
+ * const editor = createEditor({
72
+ * value: 'const x = 1',
73
+ * language: 'typescript',
74
+ * theme: 'dark',
75
+ * })
76
+ *
77
+ * editor.value() // reactive
78
+ * editor.value.set('new') // updates editor
79
+ *
80
+ * <CodeEditor instance={editor} />
81
+ * ```
82
+ */
83
+ export function createEditor(config: EditorConfig = {}): EditorInstance {
84
+ const {
85
+ value: initialValue = '',
86
+ language: initialLanguage = 'plain',
87
+ theme: initialTheme = 'light',
88
+ lineNumbers: showLineNumbers = true,
89
+ readOnly: initialReadOnly = false,
90
+ foldGutter: showFoldGutter = true,
91
+ bracketMatching: enableBracketMatching = true,
92
+ autocomplete: enableAutocomplete = true,
93
+ search: _enableSearch = true,
94
+ highlightIndentGuides: enableIndentGuides = true,
95
+ vim: enableVim = false,
96
+ emacs: enableEmacs = false,
97
+ tabSize: configTabSize = 2,
98
+ lineWrapping: enableLineWrapping = false,
99
+ placeholder: placeholderText,
100
+ minimap: enableMinimap = false,
101
+ extensions: userExtensions = [],
102
+ onChange,
103
+ } = config
104
+
105
+ // ── Reactive state ───────────────────────────────────────────────────
106
+
107
+ const value = signal(initialValue)
108
+ const language = signal<EditorLanguage>(initialLanguage)
109
+ const theme = signal<EditorTheme>(initialTheme)
110
+ const readOnly = signal(initialReadOnly)
111
+ const focused = signal(false)
112
+ const view = signal<EditorView | null>(null)
113
+
114
+ // Internal version tracker for cursor/selection reactivity
115
+ const docVersion = signal(0)
116
+
117
+ // ── Compartments (for dynamic reconfiguration) ─────────────────────
118
+
119
+ const languageCompartment = new Compartment()
120
+ const themeCompartment = new Compartment()
121
+ const readOnlyCompartment = new Compartment()
122
+ const extraKeymapCompartment = new Compartment()
123
+ const keyModeCompartment = new Compartment()
124
+
125
+ // ── Computed ─────────────────────────────────────────────────────────
126
+
127
+ const cursor = computed(() => {
128
+ docVersion() // subscribe to changes
129
+ const v = view.peek()
130
+ if (!v) return { line: 1, col: 1 }
131
+ const pos = v.state.selection.main.head
132
+ const line = v.state.doc.lineAt(pos)
133
+ return { line: line.number, col: pos - line.from + 1 }
134
+ })
135
+
136
+ const selection = computed(() => {
137
+ docVersion()
138
+ const v = view.peek()
139
+ if (!v) return { from: 0, to: 0, text: '' }
140
+ const sel = v.state.selection.main
141
+ return {
142
+ from: sel.from,
143
+ to: sel.to,
144
+ text: v.state.sliceDoc(sel.from, sel.to),
145
+ }
146
+ })
147
+
148
+ const lineCount = computed(() => {
149
+ docVersion()
150
+ const v = view.peek()
151
+ return v ? v.state.doc.lines : initialValue.split('\n').length
152
+ })
153
+
154
+ // ── Line highlight support ──────────────────────────────────────────
155
+
156
+ const lineHighlights = new Map<number, string>()
157
+
158
+ const lineHighlightField = ViewPlugin.fromClass(
159
+ class {
160
+ decorations: DecorationSet
161
+
162
+ constructor(editorView: EditorView) {
163
+ this.decorations = this.buildDecos(editorView)
164
+ }
165
+
166
+ buildDecos(editorView: EditorView): DecorationSet {
167
+ const ranges: Array<{ from: number; deco: any }> = []
168
+ for (const [lineNum, cls] of lineHighlights) {
169
+ if (lineNum >= 1 && lineNum <= editorView.state.doc.lines) {
170
+ const lineInfo = editorView.state.doc.line(lineNum)
171
+ ranges.push({
172
+ from: lineInfo.from,
173
+ deco: Decoration.line({ class: cls }),
174
+ })
175
+ }
176
+ }
177
+ return Decoration.set(
178
+ ranges
179
+ .sort((a, b) => a.from - b.from)
180
+ .map((d) => d.deco.range(d.from)),
181
+ )
182
+ }
183
+
184
+ update(upd: ViewUpdate) {
185
+ if (upd.docChanged || upd.viewportChanged) {
186
+ this.decorations = this.buildDecos(upd.view)
187
+ }
188
+ }
189
+ },
190
+ { decorations: (plugin) => plugin.decorations },
191
+ )
192
+
193
+ // ── Gutter marker support ──────────────────────────────────────────
194
+
195
+ const gutterMarkers = new Map<
196
+ number,
197
+ { class?: string; text?: string; title?: string }
198
+ >()
199
+
200
+ class CustomGutterMarker extends CMGutterMarker {
201
+ markerText: string
202
+ markerTitle: string
203
+ markerClass: string
204
+
205
+ constructor(opts: { class?: string; text?: string; title?: string }) {
206
+ super()
207
+ this.markerText = opts.text ?? ''
208
+ this.markerTitle = opts.title ?? ''
209
+ this.markerClass = opts.class ?? ''
210
+ }
211
+
212
+ toDOM() {
213
+ const el = document.createElement('span')
214
+ el.textContent = this.markerText
215
+ el.title = this.markerTitle
216
+ if (this.markerClass) el.className = this.markerClass
217
+ el.style.cssText =
218
+ 'cursor: pointer; display: inline-block; width: 100%; text-align: center;'
219
+ return el
220
+ }
221
+ }
222
+
223
+ const gutterMarkerExtension = gutter({
224
+ class: 'pyreon-code-gutter-markers',
225
+ lineMarker: (gutterView, line) => {
226
+ const lineNo = gutterView.state.doc.lineAt(line.from).number
227
+ const marker = gutterMarkers.get(lineNo)
228
+ if (!marker) return null
229
+ return new CustomGutterMarker(marker)
230
+ },
231
+ initialSpacer: () => new CustomGutterMarker({ text: ' ' }),
232
+ })
233
+
234
+ // ── Build extensions ─────────────────────────────────────────────────
235
+
236
+ function buildExtensions(langExt: Extension): Extension[] {
237
+ const exts: Extension[] = [
238
+ // Core
239
+ history(),
240
+ drawSelection(),
241
+ dropCursor(),
242
+ rectangularSelection(),
243
+ crosshairCursor(),
244
+ highlightActiveLine(),
245
+ highlightActiveLineGutter(),
246
+ highlightSelectionMatches(),
247
+ indentOnInput(),
248
+ syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
249
+ indentUnit.of(' '.repeat(configTabSize)),
250
+
251
+ // Keymaps
252
+ keymap.of([
253
+ ...closeBracketsKeymap,
254
+ ...defaultKeymap,
255
+ ...searchKeymap,
256
+ ...historyKeymap,
257
+ ...foldKeymap,
258
+ ...completionKeymap,
259
+ ...lintKeymap,
260
+ indentWithTab,
261
+ ]),
262
+
263
+ // Dynamic compartments
264
+ languageCompartment.of(langExt),
265
+ themeCompartment.of(resolveTheme(initialTheme)),
266
+ readOnlyCompartment.of(EditorState.readOnly.of(initialReadOnly)),
267
+ extraKeymapCompartment.of([]),
268
+ keyModeCompartment.of([]),
269
+
270
+ // Update listener — sync CM changes to signal
271
+ EditorView.updateListener.of((update) => {
272
+ if (update.docChanged) {
273
+ const newValue = update.state.doc.toString()
274
+ // Avoid infinite loop: only set if different
275
+ if (newValue !== value.peek()) {
276
+ value.set(newValue)
277
+ onChange?.(newValue)
278
+ }
279
+ docVersion.update((v) => v + 1)
280
+ }
281
+ if (update.selectionSet) {
282
+ docVersion.update((v) => v + 1)
283
+ }
284
+ if (update.focusChanged) {
285
+ focused.set(update.view.hasFocus)
286
+ }
287
+ }),
288
+ ]
289
+
290
+ // Optional features
291
+ if (showLineNumbers) exts.push(lineNumbers())
292
+ if (showFoldGutter) exts.push(foldGutter())
293
+ if (enableBracketMatching) exts.push(bracketMatching(), closeBrackets())
294
+ if (enableAutocomplete) exts.push(autocompletion())
295
+ if (enableLineWrapping) exts.push(EditorView.lineWrapping)
296
+ // Indent guides via theme (CM6 doesn't have a built-in extension for this)
297
+ if (enableIndentGuides) {
298
+ exts.push(
299
+ EditorView.theme({
300
+ '.cm-line': {
301
+ backgroundImage:
302
+ 'linear-gradient(to right, #e5e7eb 1px, transparent 1px)',
303
+ backgroundSize: `${configTabSize}ch 100%`,
304
+ backgroundPosition: '0 0',
305
+ },
306
+ }),
307
+ )
308
+ }
309
+ if (placeholderText) exts.push(placeholderExt(placeholderText))
310
+ if (enableMinimap) exts.push(minimapExtension())
311
+
312
+ // Line highlight decoration support
313
+ exts.push(lineHighlightField)
314
+ // Gutter marker support
315
+ exts.push(gutterMarkerExtension)
316
+
317
+ // User extensions
318
+ exts.push(...userExtensions)
319
+
320
+ return exts
321
+ }
322
+
323
+ // ── Mount helper — called by CodeEditor component ────────────────────
324
+
325
+ let mounted = false
326
+
327
+ async function mount(parent: HTMLElement): Promise<void> {
328
+ if (mounted) return
329
+
330
+ const langExt = await loadLanguage(language.peek())
331
+ const extensions = buildExtensions(langExt)
332
+
333
+ const state = EditorState.create({
334
+ doc: value.peek(),
335
+ extensions,
336
+ })
337
+
338
+ const editorView = new EditorView({
339
+ state,
340
+ parent,
341
+ })
342
+
343
+ view.set(editorView)
344
+ mounted = true
345
+
346
+ // Sync signal → editor for value changes from outside
347
+ effect(() => {
348
+ const val = value()
349
+ const v = view.peek()
350
+ if (!v) return
351
+ const current = v.state.doc.toString()
352
+ if (val !== current) {
353
+ v.dispatch({
354
+ changes: { from: 0, to: current.length, insert: val },
355
+ })
356
+ }
357
+ })
358
+
359
+ // Sync language changes
360
+ effect(() => {
361
+ const lang = language()
362
+ const v = view.peek()
363
+ if (!v) return
364
+ loadLanguage(lang).then((ext) => {
365
+ v.dispatch({ effects: languageCompartment.reconfigure(ext) })
366
+ })
367
+ })
368
+
369
+ // Sync theme changes
370
+ effect(() => {
371
+ const t = theme()
372
+ const v = view.peek()
373
+ if (!v) return
374
+ v.dispatch({ effects: themeCompartment.reconfigure(resolveTheme(t)) })
375
+ })
376
+
377
+ // Sync readOnly changes
378
+ effect(() => {
379
+ const ro = readOnly()
380
+ const v = view.peek()
381
+ if (!v) return
382
+ v.dispatch({
383
+ effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(ro)),
384
+ })
385
+ })
386
+ }
387
+
388
+ // ── Actions ──────────────────────────────────────────────────────────
389
+
390
+ function focus(): void {
391
+ view.peek()?.focus()
392
+ }
393
+
394
+ function insert(text: string): void {
395
+ const v = view.peek()
396
+ if (!v) return
397
+ const pos = v.state.selection.main.head
398
+ v.dispatch({ changes: { from: pos, insert: text } })
399
+ }
400
+
401
+ function replaceSelection(text: string): void {
402
+ const v = view.peek()
403
+ if (!v) return
404
+ v.dispatch(v.state.replaceSelection(text))
405
+ }
406
+
407
+ function select(from: number, to: number): void {
408
+ const v = view.peek()
409
+ if (!v) return
410
+ v.dispatch({ selection: { anchor: from, head: to } })
411
+ }
412
+
413
+ function selectAll(): void {
414
+ const v = view.peek()
415
+ if (!v) return
416
+ v.dispatch({ selection: { anchor: 0, head: v.state.doc.length } })
417
+ }
418
+
419
+ function goToLine(line: number): void {
420
+ const v = view.peek()
421
+ if (!v) return
422
+ const lineInfo = v.state.doc.line(
423
+ Math.min(Math.max(1, line), v.state.doc.lines),
424
+ )
425
+ v.dispatch({
426
+ selection: { anchor: lineInfo.from },
427
+ scrollIntoView: true,
428
+ })
429
+ v.focus()
430
+ }
431
+
432
+ function undo(): void {
433
+ const v = view.peek()
434
+ if (v) cmUndo(v)
435
+ }
436
+
437
+ function redo(): void {
438
+ const v = view.peek()
439
+ if (v) cmRedo(v)
440
+ }
441
+
442
+ function foldAll(): void {
443
+ const v = view.peek()
444
+ if (!v) return
445
+ const { foldAll: foldAllCmd } = require('@codemirror/language')
446
+ foldAllCmd(v)
447
+ }
448
+
449
+ function unfoldAll(): void {
450
+ const v = view.peek()
451
+ if (!v) return
452
+ const { unfoldAll: unfoldAllCmd } = require('@codemirror/language')
453
+ unfoldAllCmd(v)
454
+ }
455
+
456
+ // ── Diagnostics ────────────────────────────────────────────────────
457
+
458
+ function setDiagnostics(diagnostics: import('./types').Diagnostic[]): void {
459
+ const v = view.peek()
460
+ if (!v) return
461
+ v.dispatch(
462
+ cmSetDiagnostics(
463
+ v.state,
464
+ diagnostics.map((d) => ({
465
+ from: d.from,
466
+ to: d.to,
467
+ severity: d.severity === 'hint' ? 'info' : d.severity,
468
+ message: d.message,
469
+ source: d.source,
470
+ })),
471
+ ),
472
+ )
473
+ }
474
+
475
+ function clearDiagnostics(): void {
476
+ const v = view.peek()
477
+ if (!v) return
478
+ v.dispatch(cmSetDiagnostics(v.state, []))
479
+ }
480
+
481
+ // ── Line highlights ────────────────────────────────────────────────
482
+
483
+ function highlightLine(line: number, className: string): void {
484
+ lineHighlights.set(line, className)
485
+ // Force re-render of decorations
486
+ const v = view.peek()
487
+ if (v) v.dispatch({ effects: [] })
488
+ }
489
+
490
+ function clearLineHighlights(): void {
491
+ lineHighlights.clear()
492
+ const v = view.peek()
493
+ if (v) v.dispatch({ effects: [] })
494
+ }
495
+
496
+ // ── Gutter markers ────────────────────────────────────────────────
497
+
498
+ function setGutterMarker(
499
+ line: number,
500
+ marker: import('./types').GutterMarker,
501
+ ): void {
502
+ gutterMarkers.set(line, marker)
503
+ const v = view.peek()
504
+ if (v) v.dispatch({ effects: [] })
505
+ }
506
+
507
+ function clearGutterMarkers(): void {
508
+ gutterMarkers.clear()
509
+ const v = view.peek()
510
+ if (v) v.dispatch({ effects: [] })
511
+ }
512
+
513
+ // ── Custom keybindings ─────────────────────────────────────────────
514
+
515
+ const customKeybindings: Array<{ key: string; run: () => boolean }> = []
516
+
517
+ function addKeybinding(
518
+ key: string,
519
+ handler: () => boolean | undefined,
520
+ ): void {
521
+ customKeybindings.push({
522
+ key,
523
+ run: () => {
524
+ handler()
525
+ return true
526
+ },
527
+ })
528
+ const v = view.peek()
529
+ if (!v) return
530
+ v.dispatch({
531
+ effects: extraKeymapCompartment.reconfigure(keymap.of(customKeybindings)),
532
+ })
533
+ }
534
+
535
+ // ── Text queries ───────────────────────────────────────────────────
536
+
537
+ function getLine(line: number): string {
538
+ const v = view.peek()
539
+ if (!v) return ''
540
+ const clamped = Math.min(Math.max(1, line), v.state.doc.lines)
541
+ return v.state.doc.line(clamped).text
542
+ }
543
+
544
+ function getWordAtCursor(): string {
545
+ const v = view.peek()
546
+ if (!v) return ''
547
+ const pos = v.state.selection.main.head
548
+ const line = v.state.doc.lineAt(pos)
549
+ const col = pos - line.from
550
+ const text = line.text
551
+
552
+ // Find word boundaries
553
+ let start = col
554
+ let end = col
555
+ while (start > 0 && /\w/.test(text[start - 1]!)) start--
556
+ while (end < text.length && /\w/.test(text[end]!)) end++
557
+
558
+ return text.slice(start, end)
559
+ }
560
+
561
+ function scrollTo(pos: number): void {
562
+ const v = view.peek()
563
+ if (!v) return
564
+ v.dispatch({
565
+ effects: EditorView.scrollIntoView(pos, { y: 'center' }),
566
+ })
567
+ }
568
+
569
+ // ── Vim / Emacs mode loading ───────────────────────────────────────
570
+
571
+ async function loadKeyMode(): Promise<void> {
572
+ const v = view.peek()
573
+ if (!v) return
574
+
575
+ // Use string concat to prevent Vite from statically analyzing these optional imports
576
+ const vimPkg = '@replit/codemirror-' + 'vim'
577
+ const emacsPkg = '@replit/codemirror-' + 'emacs'
578
+
579
+ if (enableVim) {
580
+ try {
581
+ const mod = await import(/* @vite-ignore */ vimPkg)
582
+ v.dispatch({
583
+ effects: keyModeCompartment.reconfigure(mod.vim()),
584
+ })
585
+ } catch {
586
+ /* @replit/codemirror-vim not installed */
587
+ }
588
+ }
589
+
590
+ if (enableEmacs) {
591
+ try {
592
+ const mod = await import(/* @vite-ignore */ emacsPkg)
593
+ v.dispatch({
594
+ effects: keyModeCompartment.reconfigure(mod.emacs()),
595
+ })
596
+ } catch {
597
+ /* @replit/codemirror-emacs not installed */
598
+ }
599
+ }
600
+ }
601
+
602
+ function dispose(): void {
603
+ const v = view.peek()
604
+ if (v) {
605
+ v.destroy()
606
+ view.set(null)
607
+ mounted = false
608
+ }
609
+ }
610
+
611
+ // ── Expose mount for component ─────────────────────────────────────
612
+
613
+ const instance: EditorInstance & { _mount: typeof mount } = {
614
+ value,
615
+ language,
616
+ theme,
617
+ readOnly,
618
+ cursor,
619
+ selection,
620
+ lineCount,
621
+ focused,
622
+ view,
623
+ focus,
624
+ insert,
625
+ replaceSelection,
626
+ select,
627
+ selectAll,
628
+ goToLine,
629
+ undo,
630
+ redo,
631
+ foldAll,
632
+ unfoldAll,
633
+ setDiagnostics,
634
+ clearDiagnostics,
635
+ highlightLine,
636
+ clearLineHighlights,
637
+ setGutterMarker,
638
+ clearGutterMarkers,
639
+ addKeybinding,
640
+ getLine,
641
+ getWordAtCursor,
642
+ scrollTo,
643
+ config,
644
+ dispose,
645
+ _mount: async (parent: HTMLElement) => {
646
+ await mount(parent)
647
+ await loadKeyMode()
648
+ },
649
+ }
650
+
651
+ return instance
652
+ }
package/src/index.ts ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * @pyreon/code — Reactive code editor for Pyreon.
3
+ *
4
+ * CodeMirror 6 with signal-backed state, lazy-loaded languages,
5
+ * custom minimap, and diff editor. ~250KB for a full-featured
6
+ * code editor instead of ~2.5MB for Monaco.
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * import { createEditor, CodeEditor } from '@pyreon/code'
11
+ *
12
+ * const editor = createEditor({
13
+ * value: 'const x = 1',
14
+ * language: 'typescript',
15
+ * theme: 'dark',
16
+ * minimap: true,
17
+ * })
18
+ *
19
+ * editor.value() // reactive signal
20
+ * editor.value.set('new') // updates editor
21
+ *
22
+ * <CodeEditor instance={editor} style="height: 400px" />
23
+ * ```
24
+ */
25
+
26
+ // Components
27
+ export { CodeEditor } from './components/code-editor'
28
+ export { DiffEditor } from './components/diff-editor'
29
+ export { TabbedEditor } from './components/tabbed-editor'
30
+ // Core
31
+ export { createEditor } from './editor'
32
+ // Languages
33
+ export { getAvailableLanguages, loadLanguage } from './languages'
34
+ // Minimap
35
+ export { minimapExtension } from './minimap'
36
+ // Themes
37
+ export { darkTheme, lightTheme, resolveTheme } from './themes'
38
+
39
+ // Types
40
+ export type {
41
+ CodeEditorProps,
42
+ DiffEditorProps,
43
+ EditorConfig,
44
+ EditorInstance,
45
+ EditorLanguage,
46
+ EditorTheme,
47
+ GutterMarker,
48
+ Tab,
49
+ TabbedEditorConfig,
50
+ TabbedEditorInstance,
51
+ TabbedEditorProps,
52
+ } from './types'