@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.
- package/LICENSE +21 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/dist-B5vB-rif.js +3904 -0
- package/lib/dist-B5vB-rif.js.map +1 -0
- package/lib/dist-BAfzu5eu.js +1428 -0
- package/lib/dist-BAfzu5eu.js.map +1 -0
- package/lib/dist-BLlV_D16.js +1166 -0
- package/lib/dist-BLlV_D16.js.map +1 -0
- package/lib/dist-BNmKLTu8.js +373 -0
- package/lib/dist-BNmKLTu8.js.map +1 -0
- package/lib/dist-BZtTlC1J.js +692 -0
- package/lib/dist-BZtTlC1J.js.map +1 -0
- package/lib/dist-CTDqGIAf.js +856 -0
- package/lib/dist-CTDqGIAf.js.map +1 -0
- package/lib/dist-CTPisNZp.js +83 -0
- package/lib/dist-CTPisNZp.js.map +1 -0
- package/lib/dist-Ce2tvOxv.js +379 -0
- package/lib/dist-Ce2tvOxv.js.map +1 -0
- package/lib/dist-CttF0OTv.js +465 -0
- package/lib/dist-CttF0OTv.js.map +1 -0
- package/lib/dist-DS2tluW9.js +818 -0
- package/lib/dist-DS2tluW9.js.map +1 -0
- package/lib/dist-DUNx9ldu.js +460 -0
- package/lib/dist-DUNx9ldu.js.map +1 -0
- package/lib/dist-Dej_yf3k.js +473 -0
- package/lib/dist-Dej_yf3k.js.map +1 -0
- package/lib/dist-DshStUxU.js +283 -0
- package/lib/dist-DshStUxU.js.map +1 -0
- package/lib/dist-qTrOe7xY.js +461 -0
- package/lib/dist-qTrOe7xY.js.map +1 -0
- package/lib/dist-v09vikKr.js +2421 -0
- package/lib/dist-v09vikKr.js.map +1 -0
- package/lib/index.js +915 -0
- package/lib/index.js.map +1 -0
- package/lib/types/dist.d.ts +798 -0
- package/lib/types/dist.d.ts.map +1 -0
- package/lib/types/dist10.d.ts +67 -0
- package/lib/types/dist10.d.ts.map +1 -0
- package/lib/types/dist11.d.ts +126 -0
- package/lib/types/dist11.d.ts.map +1 -0
- package/lib/types/dist12.d.ts +21 -0
- package/lib/types/dist12.d.ts.map +1 -0
- package/lib/types/dist13.d.ts +404 -0
- package/lib/types/dist13.d.ts.map +1 -0
- package/lib/types/dist14.d.ts +292 -0
- package/lib/types/dist14.d.ts.map +1 -0
- package/lib/types/dist15.d.ts +132 -0
- package/lib/types/dist15.d.ts.map +1 -0
- package/lib/types/dist2.d.ts +15 -0
- package/lib/types/dist2.d.ts.map +1 -0
- package/lib/types/dist3.d.ts +106 -0
- package/lib/types/dist3.d.ts.map +1 -0
- package/lib/types/dist4.d.ts +67 -0
- package/lib/types/dist4.d.ts.map +1 -0
- package/lib/types/dist5.d.ts +95 -0
- package/lib/types/dist5.d.ts.map +1 -0
- package/lib/types/dist6.d.ts +330 -0
- package/lib/types/dist6.d.ts.map +1 -0
- package/lib/types/dist7.d.ts +15 -0
- package/lib/types/dist7.d.ts.map +1 -0
- package/lib/types/dist8.d.ts +15 -0
- package/lib/types/dist8.d.ts.map +1 -0
- package/lib/types/dist9.d.ts +635 -0
- package/lib/types/dist9.d.ts.map +1 -0
- package/lib/types/index.d.ts +852 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index2.d.ts +347 -0
- package/lib/types/index2.d.ts.map +1 -0
- package/package.json +79 -0
- package/src/components/code-editor.tsx +42 -0
- package/src/components/diff-editor.tsx +97 -0
- package/src/components/tabbed-editor.tsx +86 -0
- package/src/editor.ts +652 -0
- package/src/index.ts +52 -0
- package/src/languages.ts +77 -0
- package/src/minimap.ts +160 -0
- package/src/tabbed-editor.ts +231 -0
- package/src/tests/code.test.ts +505 -0
- package/src/themes.ts +87 -0
- 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'
|