@pyreon/code 0.6.0 → 0.7.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.
@@ -1,852 +1,347 @@
1
- import { bracketMatching, defaultHighlightStyle, foldGutter, foldKeymap, indentOnInput, indentUnit, syntaxHighlighting } from "@codemirror/language";
2
- import { MergeView } from "@codemirror/merge";
3
- import { Compartment, EditorState } from "@codemirror/state";
4
- import { Decoration, EditorView, GutterMarker, ViewPlugin, crosshairCursor, drawSelection, dropCursor, gutter, highlightActiveLine, highlightActiveLineGutter, keymap, lineNumbers, placeholder, rectangularSelection } from "@codemirror/view";
5
- import { autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap } from "@codemirror/autocomplete";
6
- import { defaultKeymap, history, historyKeymap, indentWithTab, redo, undo } from "@codemirror/commands";
7
- import { lintKeymap, setDiagnostics } from "@codemirror/lint";
8
- import { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
9
- import { computed, effect, signal } from "@pyreon/reactivity";
1
+ import { VNodeChild } from "@pyreon/core";
2
+ import { Extension } from "@codemirror/state";
3
+ import { EditorView } from "@codemirror/view";
4
+ import { Computed, Signal } from "@pyreon/reactivity";
10
5
 
11
- //#region \0rolldown/runtime.js
12
-
13
- function h(type, props, ...children) {
14
- return {
15
- type,
16
- props: props ?? EMPTY_PROPS,
17
- children: normalizeChildren(children),
18
- key: props?.key ?? null
19
- };
6
+ //#region src/types.d.ts
7
+ type EditorLanguage = 'javascript' | 'typescript' | 'jsx' | 'tsx' | 'html' | 'css' | 'json' | 'markdown' | 'python' | 'rust' | 'sql' | 'xml' | 'yaml' | 'cpp' | 'java' | 'go' | 'php' | 'ruby' | 'shell' | 'plain';
8
+ type EditorTheme = 'light' | 'dark' | Extension;
9
+ interface EditorConfig {
10
+ /** Initial value */
11
+ value?: string;
12
+ /** Language for syntax highlighting — lazy-loaded */
13
+ language?: EditorLanguage;
14
+ /** Theme — 'light', 'dark', or a custom CodeMirror theme extension */
15
+ theme?: EditorTheme;
16
+ /** Show line numbers — default: true */
17
+ lineNumbers?: boolean;
18
+ /** Read-only mode — default: false */
19
+ readOnly?: boolean;
20
+ /** Enable code folding — default: true */
21
+ foldGutter?: boolean;
22
+ /** Enable bracket matching — default: true */
23
+ bracketMatching?: boolean;
24
+ /** Enable autocomplete — default: true */
25
+ autocomplete?: boolean;
26
+ /** Enable search (Cmd+F) — default: true */
27
+ search?: boolean;
28
+ /** Enable lint/diagnostics — default: false */
29
+ lint?: boolean;
30
+ /** Enable indent guides — default: true */
31
+ highlightIndentGuides?: boolean;
32
+ /** Vim keybinding mode — default: false */
33
+ vim?: boolean;
34
+ /** Emacs keybinding mode — default: false */
35
+ emacs?: boolean;
36
+ /** Tab size — default: 2 */
37
+ tabSize?: number;
38
+ /** Enable indent guides — default: true */
39
+ indentGuides?: boolean;
40
+ /** Enable line wrapping — default: false */
41
+ lineWrapping?: boolean;
42
+ /** Placeholder text when empty */
43
+ placeholder?: string;
44
+ /** Enable minimap — default: false */
45
+ minimap?: boolean;
46
+ /** Additional CodeMirror extensions */
47
+ extensions?: Extension[];
48
+ /** Called when value changes */
49
+ onChange?: (value: string) => void;
20
50
  }
21
- function normalizeChildren(children) {
22
- for (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);
23
- return children;
51
+ interface EditorInstance {
52
+ /** Current editor value reactive signal */
53
+ value: Signal<string>;
54
+ /** Current language — reactive signal */
55
+ language: Signal<EditorLanguage>;
56
+ /** Current theme — reactive signal */
57
+ theme: Signal<EditorTheme>;
58
+ /** Read-only state — reactive signal */
59
+ readOnly: Signal<boolean>;
60
+ /** Cursor position — reactive */
61
+ cursor: Computed<{
62
+ line: number;
63
+ col: number;
64
+ }>;
65
+ /** Current selection — reactive */
66
+ selection: Computed<{
67
+ from: number;
68
+ to: number;
69
+ text: string;
70
+ }>;
71
+ /** Line count — reactive */
72
+ lineCount: Computed<number>;
73
+ /** Whether the editor has focus — reactive */
74
+ focused: Signal<boolean>;
75
+ /** The underlying CodeMirror EditorView — null until mounted */
76
+ view: Signal<EditorView | null>;
77
+ /** Focus the editor */
78
+ focus: () => void;
79
+ /** Insert text at cursor */
80
+ insert: (text: string) => void;
81
+ /** Replace selection */
82
+ replaceSelection: (text: string) => void;
83
+ /** Select a range */
84
+ select: (from: number, to: number) => void;
85
+ /** Select all */
86
+ selectAll: () => void;
87
+ /** Go to a specific line */
88
+ goToLine: (line: number) => void;
89
+ /** Undo */
90
+ undo: () => void;
91
+ /** Redo */
92
+ redo: () => void;
93
+ /** Fold all */
94
+ foldAll: () => void;
95
+ /** Unfold all */
96
+ unfoldAll: () => void;
97
+ /** Set diagnostics (lint errors/warnings) */
98
+ setDiagnostics: (diagnostics: Diagnostic[]) => void;
99
+ /** Clear all diagnostics */
100
+ clearDiagnostics: () => void;
101
+ /** Highlight a specific line (e.g., error line, current execution) */
102
+ highlightLine: (line: number, className: string) => void;
103
+ /** Clear all line highlights */
104
+ clearLineHighlights: () => void;
105
+ /** Set gutter markers (breakpoints, error icons) */
106
+ setGutterMarker: (line: number, marker: GutterMarker) => void;
107
+ /** Clear all gutter markers */
108
+ clearGutterMarkers: () => void;
109
+ /** Add a custom keybinding */
110
+ addKeybinding: (key: string, handler: () => boolean | undefined) => void;
111
+ /** Get the text of a specific line */
112
+ getLine: (line: number) => string;
113
+ /** Get word at cursor position */
114
+ getWordAtCursor: () => string;
115
+ /** Scroll to a specific position */
116
+ scrollTo: (pos: number) => void;
117
+ /** The editor configuration */
118
+ config: EditorConfig;
119
+ /** Dispose — clean up view and listeners */
120
+ dispose: () => void;
24
121
  }
25
- function flattenChildren(children) {
26
- const result = [];
27
- for (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));else result.push(child);
28
- return result;
122
+ interface Diagnostic {
123
+ /** Start position (character offset) */
124
+ from: number;
125
+ /** End position (character offset) */
126
+ to: number;
127
+ /** Severity */
128
+ severity: 'error' | 'warning' | 'info' | 'hint';
129
+ /** Message */
130
+ message: string;
131
+ /** Optional source (e.g., "typescript", "eslint") */
132
+ source?: string;
29
133
  }
30
- /**
31
- * JSX automatic runtime.
32
- *
33
- * When tsconfig has `"jsxImportSource": "@pyreon/core"`, the TS/bundler compiler
34
- * rewrites JSX to imports from this file automatically:
35
- * <div class="x" /> → jsx("div", { class: "x" })
36
- */
37
- function jsx(type, props, key) {
38
- const {
39
- children,
40
- ...rest
41
- } = props;
42
- const propsWithKey = key != null ? {
43
- ...rest,
44
- key
45
- } : rest;
46
- if (typeof type === "function") return h(type, children !== void 0 ? {
47
- ...propsWithKey,
48
- children
49
- } : propsWithKey);
50
- return h(type, propsWithKey, ...(children === void 0 ? [] : Array.isArray(children) ? children : [children]));
134
+ interface GutterMarker {
135
+ /** CSS class for the marker element */
136
+ class?: string;
137
+ /** Text content (e.g., emoji or icon) */
138
+ text?: string;
139
+ /** Tooltip on hover */
140
+ title?: string;
51
141
  }
52
- //#endregion
53
- //#region src/components/code-editor.tsx
54
- /**
55
- * Code editor component — mounts a CodeMirror 6 instance.
56
- *
57
- * @example
58
- * ```tsx
59
- * const editor = createEditor({
60
- * value: 'const x = 1',
61
- * language: 'typescript',
62
- * theme: 'dark',
63
- * })
64
- *
65
- * <CodeEditor instance={editor} style="height: 400px" />
66
- * ```
67
- */
68
- function CodeEditor(props) {
69
- const {
70
- instance
71
- } = props;
72
- const containerRef = el => {
73
- if (!el) return;
74
- const mountable = instance;
75
- if (mountable._mount) mountable._mount(el);
76
- };
77
- const baseStyle = `width: 100%; height: 100%; overflow: hidden; ${props.style ?? ""}`;
78
- return /* @__PURE__ */jsx("div", {
79
- ref: containerRef,
80
- class: `pyreon-code-editor ${props.class ?? ""}`,
81
- style: baseStyle
82
- });
142
+ interface CodeEditorProps {
143
+ instance: EditorInstance;
144
+ style?: string;
145
+ class?: string;
83
146
  }
84
-
85
- //#endregion
86
- //#region src/languages.ts
87
- /**
88
- * Language extension loaders — lazy-loaded on demand.
89
- * Only the requested language is imported, keeping the initial bundle small.
90
- */
91
-
92
- /**
93
- * Load a language extension. Returns cached if already loaded.
94
- * Language grammars are lazy-imported — zero cost until used.
95
- *
96
- * @example
97
- * ```ts
98
- * const ext = await loadLanguage('typescript')
99
- * ```
100
- */
101
- async function loadLanguage(language) {
102
- const cached = loaded.get(language);
103
- if (cached) return cached;
104
- const loader = languageLoaders[language];
105
- if (!loader) return [];
106
- try {
107
- const ext = await loader();
108
- loaded.set(language, ext);
109
- return ext;
110
- } catch {
111
- return [];
112
- }
147
+ interface DiffEditorProps {
148
+ /** Original (left) content */
149
+ original: string | Signal<string>;
150
+ /** Modified (right) content */
151
+ modified: string | Signal<string>;
152
+ /** Language for both panels */
153
+ language?: EditorLanguage;
154
+ /** Theme */
155
+ theme?: EditorTheme;
156
+ /** Show inline diff instead of side-by-side default: false */
157
+ inline?: boolean;
158
+ /** Read-only — default: true */
159
+ readOnly?: boolean;
160
+ style?: string;
161
+ class?: string;
113
162
  }
114
- /**
115
- * Get available languages.
116
- */
117
- function getAvailableLanguages() {
118
- return Object.keys(languageLoaders);
163
+ interface Tab {
164
+ /** Unique tab identifier — defaults to name */
165
+ id?: string;
166
+ /** File name displayed in the tab */
167
+ name: string;
168
+ /** Language for syntax highlighting */
169
+ language?: EditorLanguage;
170
+ /** File content */
171
+ value: string;
172
+ /** Whether the tab has unsaved changes */
173
+ modified?: boolean;
174
+ /** Whether the tab can be closed — default: true */
175
+ closable?: boolean;
176
+ }
177
+ interface TabbedEditorConfig {
178
+ /** Initial tabs */
179
+ tabs?: Tab[];
180
+ /** Theme — 'light', 'dark', or custom */
181
+ theme?: EditorTheme;
182
+ /** Editor config applied to all tabs */
183
+ editorConfig?: Omit<EditorConfig, 'value' | 'language' | 'theme'>;
184
+ }
185
+ interface TabbedEditorInstance {
186
+ /** The underlying editor instance */
187
+ editor: EditorInstance;
188
+ /** All open tabs — reactive */
189
+ tabs: Signal<Tab[]>;
190
+ /** Active tab — reactive */
191
+ activeTab: Computed<Tab | null>;
192
+ /** Active tab ID — reactive */
193
+ activeTabId: Signal<string>;
194
+ /** Open a new tab (or switch to it if already open) */
195
+ openTab: (tab: Tab) => void;
196
+ /** Close a tab by ID */
197
+ closeTab: (id: string) => void;
198
+ /** Switch to a tab by ID */
199
+ switchTab: (id: string) => void;
200
+ /** Rename a tab */
201
+ renameTab: (id: string, name: string) => void;
202
+ /** Mark a tab as modified/saved */
203
+ setModified: (id: string, modified: boolean) => void;
204
+ /** Reorder tabs */
205
+ moveTab: (fromIndex: number, toIndex: number) => void;
206
+ /** Get tab by ID */
207
+ getTab: (id: string) => Tab | undefined;
208
+ /** Close all tabs */
209
+ closeAll: () => void;
210
+ /** Close all tabs except the given one */
211
+ closeOthers: (id: string) => void;
212
+ /** Dispose */
213
+ dispose: () => void;
214
+ }
215
+ interface TabbedEditorProps {
216
+ instance: TabbedEditorInstance;
217
+ style?: string;
218
+ class?: string;
119
219
  }
120
-
121
220
  //#endregion
122
- //#region src/themes.ts
221
+ //#region src/components/code-editor.d.ts
123
222
  /**
124
- * Light themeclean, minimal.
125
- */
126
-
223
+ * Code editor component mounts a CodeMirror 6 instance.
224
+ *
225
+ * @example
226
+ * ```tsx
227
+ * const editor = createEditor({
228
+ * value: 'const x = 1',
229
+ * language: 'typescript',
230
+ * theme: 'dark',
231
+ * })
232
+ *
233
+ * <CodeEditor instance={editor} style="height: 400px" />
234
+ * ```
235
+ */
236
+ declare function CodeEditor(props: CodeEditorProps): VNodeChild;
237
+ //#endregion
238
+ //#region src/components/diff-editor.d.ts
127
239
  /**
128
- * Resolve a theme value to a CodeMirror extension.
129
- */
130
- function resolveTheme(theme) {
131
- if (theme === "light") return lightTheme;
132
- if (theme === "dark") return darkTheme;
133
- return theme;
134
- }
135
-
240
+ * Side-by-side or inline diff editor using @codemirror/merge.
241
+ *
242
+ * @example
243
+ * ```tsx
244
+ * <DiffEditor
245
+ * original="const x = 1"
246
+ * modified="const x = 2"
247
+ * language="typescript"
248
+ * theme="dark"
249
+ * style="height: 400px"
250
+ * />
251
+ * ```
252
+ */
253
+ declare function DiffEditor(props: DiffEditorProps): VNodeChild;
136
254
  //#endregion
137
- //#region src/components/diff-editor.tsx
255
+ //#region src/components/tabbed-editor.d.ts
138
256
  /**
139
- * Side-by-side or inline diff editor using @codemirror/merge.
140
- *
141
- * @example
142
- * ```tsx
143
- * <DiffEditor
144
- * original="const x = 1"
145
- * modified="const x = 2"
146
- * language="typescript"
147
- * theme="dark"
148
- * style="height: 400px"
149
- * />
150
- * ```
151
- */
152
- function DiffEditor(props) {
153
- const {
154
- original,
155
- modified,
156
- language = "plain",
157
- theme = "light",
158
- readOnly = true,
159
- inline = false
160
- } = props;
161
- const containerRef = async el => {
162
- if (!el) return;
163
- const langExt = await loadLanguage(language);
164
- const themeExt = resolveTheme(theme);
165
- const extensions = [syntaxHighlighting(defaultHighlightStyle, {
166
- fallback: true
167
- }), langExt, themeExt, EditorView.editable.of(!readOnly), EditorState.readOnly.of(readOnly)];
168
- const originalText = typeof original === "string" ? original : original();
169
- const modifiedText = typeof modified === "string" ? modified : modified();
170
- el.innerHTML = "";
171
- if (inline) new MergeView({
172
- a: {
173
- doc: originalText,
174
- extensions
175
- },
176
- b: {
177
- doc: modifiedText,
178
- extensions
179
- },
180
- parent: el,
181
- collapseUnchanged: {
182
- margin: 3,
183
- minSize: 4
184
- }
185
- });else new MergeView({
186
- a: {
187
- doc: originalText,
188
- extensions
189
- },
190
- b: {
191
- doc: modifiedText,
192
- extensions
193
- },
194
- parent: el,
195
- collapseUnchanged: {
196
- margin: 3,
197
- minSize: 4
198
- }
199
- });
200
- };
201
- const baseStyle = `width: 100%; height: 100%; overflow: hidden; ${props.style ?? ""}`;
202
- return /* @__PURE__ */jsx("div", {
203
- ref: containerRef,
204
- class: `pyreon-diff-editor ${props.class ?? ""}`,
205
- style: baseStyle
206
- });
207
- }
208
-
257
+ * Tabbed code editor component renders tab bar + editor.
258
+ * Headless styling — the tab bar is a plain div with button tabs.
259
+ * Consumers can style via CSS classes.
260
+ *
261
+ * @example
262
+ * ```tsx
263
+ * const editor = createTabbedEditor({
264
+ * tabs: [
265
+ * { name: 'index.ts', language: 'typescript', value: 'const x = 1' },
266
+ * { name: 'style.css', language: 'css', value: '.app { }' },
267
+ * ],
268
+ * theme: 'dark',
269
+ * })
270
+ *
271
+ * <TabbedEditor instance={editor} style="height: 500px" />
272
+ * ```
273
+ */
274
+ declare function TabbedEditor(props: TabbedEditorProps): VNodeChild;
209
275
  //#endregion
210
- //#region src/components/tabbed-editor.tsx
276
+ //#region src/editor.d.ts
211
277
  /**
212
- * Tabbed code editor component — renders tab bar + editor.
213
- * Headless styling — the tab bar is a plain div with button tabs.
214
- * Consumers can style via CSS classes.
215
- *
216
- * @example
217
- * ```tsx
218
- * const editor = createTabbedEditor({
219
- * tabs: [
220
- * { name: 'index.ts', language: 'typescript', value: 'const x = 1' },
221
- * { name: 'style.css', language: 'css', value: '.app { }' },
222
- * ],
223
- * theme: 'dark',
224
- * })
225
- *
226
- * <TabbedEditor instance={editor} style="height: 500px" />
227
- * ```
228
- */
229
- function TabbedEditor(props) {
230
- const {
231
- instance
232
- } = props;
233
- const containerStyle = `display: flex; flex-direction: column; width: 100%; height: 100%; ${props.style ?? ""}`;
234
- const tabBarStyle = "display: flex; overflow-x: auto; background: #f1f5f9; border-bottom: 1px solid #e2e8f0; min-height: 34px; flex-shrink: 0;";
235
- return /* @__PURE__ */jsxs("div", {
236
- class: `pyreon-tabbed-editor ${props.class ?? ""}`,
237
- style: containerStyle,
238
- children: [() => {
239
- const tabs = instance.tabs();
240
- const activeId = instance.activeTabId();
241
- return /* @__PURE__ */jsx("div", {
242
- class: "pyreon-tabbed-editor-tabs",
243
- style: tabBarStyle,
244
- children: tabs.map(tab => {
245
- const id = tab.id ?? tab.name;
246
- const isActive = id === activeId;
247
- const tabStyle = `display: flex; align-items: center; gap: 6px; padding: 6px 12px; border: none; background: ${isActive ? "white" : "transparent"}; border-bottom: ${isActive ? "2px solid #3b82f6" : "2px solid transparent"}; cursor: pointer; font-size: 13px; color: ${isActive ? "#1e293b" : "#64748b"}; white-space: nowrap; position: relative; font-family: inherit;`;
248
- return /* @__PURE__ */jsxs("button", {
249
- type: "button",
250
- class: `pyreon-tab ${isActive ? "active" : ""} ${tab.modified ? "modified" : ""}`,
251
- style: tabStyle,
252
- onClick: () => instance.switchTab(id),
253
- children: [/* @__PURE__ */jsx("span", {
254
- children: tab.name
255
- }), tab.modified && /* @__PURE__ */jsx("span", {
256
- style: "width: 6px; height: 6px; border-radius: 50%; background: #f59e0b; flex-shrink: 0;",
257
- title: "Modified"
258
- }), tab.closable !== false && /* @__PURE__ */jsx("span", {
259
- style: "font-size: 14px; line-height: 1; opacity: 0.5; cursor: pointer; padding: 0 2px; margin-left: 2px;",
260
- title: "Close",
261
- onClick: e => {
262
- e.stopPropagation();
263
- instance.closeTab(id);
264
- },
265
- children: "×"
266
- })]
267
- }, id);
268
- })
269
- });
270
- }, /* @__PURE__ */jsx("div", {
271
- style: "flex: 1; min-height: 0;",
272
- children: /* @__PURE__ */jsx(CodeEditor, {
273
- instance: instance.editor
274
- })
275
- })]
276
- });
277
- }
278
-
278
+ * Create a reactive code editor instance.
279
+ *
280
+ * The editor state (value, language, theme, cursor, selection) is backed
281
+ * by signals. The CodeMirror EditorView is created when mounted via
282
+ * the `<CodeEditor>` component.
283
+ *
284
+ * @param config - Editor configuration
285
+ * @returns A reactive EditorInstance
286
+ *
287
+ * @example
288
+ * ```tsx
289
+ * const editor = createEditor({
290
+ * value: 'const x = 1',
291
+ * language: 'typescript',
292
+ * theme: 'dark',
293
+ * })
294
+ *
295
+ * editor.value() // reactive
296
+ * editor.value.set('new') // updates editor
297
+ *
298
+ * <CodeEditor instance={editor} />
299
+ * ```
300
+ */
301
+ declare function createEditor(config?: EditorConfig): EditorInstance;
279
302
  //#endregion
280
- //#region src/minimap.ts
303
+ //#region src/languages.d.ts
281
304
  /**
282
- * Canvas-based minimap extension for CodeMirror 6.
283
- * Renders a scaled-down overview of the document on the right side.
284
- */
285
-
286
- function createMinimapCanvas() {
287
- const canvas = document.createElement("canvas");
288
- canvas.style.cssText = `position: absolute; right: 0; top: 0; width: ${MINIMAP_WIDTH}px; height: 100%; cursor: pointer; z-index: 5;`;
289
- canvas.width = MINIMAP_WIDTH * 2;
290
- return canvas;
291
- }
292
- function renderMinimap(canvas, view) {
293
- const ctx = canvas.getContext("2d");
294
- if (!ctx) return;
295
- const doc = view.state.doc;
296
- const totalLines = doc.lines;
297
- const height = canvas.clientHeight;
298
- canvas.height = height * 2;
299
- const isDark = view.dom.classList.contains("cm-dark");
300
- const bg = isDark ? MINIMAP_BG : MINIMAP_BG_LIGHT;
301
- const textColor = isDark ? TEXT_COLOR : TEXT_COLOR_LIGHT;
302
- const scale = 2;
303
- ctx.setTransform(scale, 0, 0, scale, 0, 0);
304
- ctx.fillStyle = bg;
305
- ctx.fillRect(0, 0, MINIMAP_WIDTH, height);
306
- const contentHeight = totalLines * LINE_HEIGHT;
307
- const scrollFraction = contentHeight > height ? view.scrollDOM.scrollTop / (view.scrollDOM.scrollHeight - view.scrollDOM.clientHeight || 1) : 0;
308
- const offset = contentHeight > height ? scrollFraction * (contentHeight - height) : 0;
309
- ctx.fillStyle = textColor;
310
- const startLine = Math.max(1, Math.floor(offset / LINE_HEIGHT));
311
- const endLine = Math.min(totalLines, startLine + Math.ceil(height / LINE_HEIGHT) + 1);
312
- for (let i = startLine; i <= endLine; i++) {
313
- const line = doc.line(i);
314
- const y = (i - 1) * LINE_HEIGHT - offset;
315
- if (y < -LINE_HEIGHT || y > height) continue;
316
- const text = line.text;
317
- let x = 4;
318
- for (let j = 0; j < Math.min(text.length, 60); j++) {
319
- if (text[j] !== " " && text[j] !== " ") ctx.fillRect(x, y, CHAR_WIDTH, 1.5);
320
- x += CHAR_WIDTH;
321
- }
322
- }
323
- const viewportTop = view.scrollDOM.scrollTop;
324
- const viewportHeight = view.scrollDOM.clientHeight;
325
- const docHeight = view.scrollDOM.scrollHeight || 1;
326
- const vpY = viewportTop / docHeight * Math.min(contentHeight, height);
327
- const vpH = viewportHeight / docHeight * Math.min(contentHeight, height);
328
- ctx.fillStyle = VIEWPORT_COLOR;
329
- ctx.fillRect(0, vpY, MINIMAP_WIDTH, vpH);
330
- ctx.strokeStyle = VIEWPORT_BORDER;
331
- ctx.lineWidth = 1;
332
- ctx.strokeRect(.5, vpY + .5, MINIMAP_WIDTH - 1, vpH - 1);
333
- }
305
+ * Load a language extension. Returns cached if already loaded.
306
+ * Language grammars are lazy-imported zero cost until used.
307
+ *
308
+ * @example
309
+ * ```ts
310
+ * const ext = await loadLanguage('typescript')
311
+ * ```
312
+ */
313
+ declare function loadLanguage(language: EditorLanguage): Promise<Extension>;
334
314
  /**
335
- * CodeMirror 6 minimap extension.
336
- * Renders a canvas-based code overview on the right side of the editor.
337
- *
338
- * @example
339
- * ```ts
340
- * import { minimapExtension } from '@pyreon/code'
341
- * // Add to editor extensions
342
- * ```
343
- */
344
- function minimapExtension() {
345
- return [ViewPlugin.fromClass(class {
346
- canvas;
347
- view;
348
- animFrame = null;
349
- constructor(view) {
350
- this.view = view;
351
- this.canvas = createMinimapCanvas();
352
- view.dom.style.position = "relative";
353
- view.dom.appendChild(this.canvas);
354
- this.canvas.addEventListener("click", e => {
355
- const rect = this.canvas.getBoundingClientRect();
356
- const scrollTarget = (e.clientY - rect.top) / rect.height * (view.scrollDOM.scrollHeight - view.scrollDOM.clientHeight);
357
- view.scrollDOM.scrollTo({
358
- top: scrollTarget,
359
- behavior: "smooth"
360
- });
361
- });
362
- this.render();
363
- }
364
- render() {
365
- renderMinimap(this.canvas, this.view);
366
- }
367
- update(update) {
368
- if (update.docChanged || update.viewportChanged || update.geometryChanged) {
369
- if (this.animFrame) cancelAnimationFrame(this.animFrame);
370
- this.animFrame = requestAnimationFrame(() => this.render());
371
- }
372
- }
373
- destroy() {
374
- if (this.animFrame) cancelAnimationFrame(this.animFrame);
375
- this.canvas.remove();
376
- }
377
- }), EditorView.theme({
378
- ".cm-scroller": {
379
- paddingRight: `${MINIMAP_WIDTH + 8}px`
380
- }
381
- })];
382
- }
383
-
315
+ * Get available languages.
316
+ */
317
+ declare function getAvailableLanguages(): EditorLanguage[];
384
318
  //#endregion
385
- //#region src/editor.ts
319
+ //#region src/minimap.d.ts
386
320
  /**
387
- * Create a reactive code editor instance.
388
- *
389
- * The editor state (value, language, theme, cursor, selection) is backed
390
- * by signals. The CodeMirror EditorView is created when mounted via
391
- * the `<CodeEditor>` component.
392
- *
393
- * @param config - Editor configuration
394
- * @returns A reactive EditorInstance
395
- *
396
- * @example
397
- * ```tsx
398
- * const editor = createEditor({
399
- * value: 'const x = 1',
400
- * language: 'typescript',
401
- * theme: 'dark',
402
- * })
403
- *
404
- * editor.value() // reactive
405
- * editor.value.set('new') // updates editor
406
- *
407
- * <CodeEditor instance={editor} />
408
- * ```
409
- */
410
- function createEditor(config = {}) {
411
- const {
412
- value: initialValue = "",
413
- language: initialLanguage = "plain",
414
- theme: initialTheme = "light",
415
- lineNumbers: showLineNumbers = true,
416
- readOnly: initialReadOnly = false,
417
- foldGutter: showFoldGutter = true,
418
- bracketMatching: enableBracketMatching = true,
419
- autocomplete: enableAutocomplete = true,
420
- search: _enableSearch = true,
421
- highlightIndentGuides: enableIndentGuides = true,
422
- vim: enableVim = false,
423
- emacs: enableEmacs = false,
424
- tabSize: configTabSize = 2,
425
- lineWrapping: enableLineWrapping = false,
426
- placeholder: placeholderText,
427
- minimap: enableMinimap = false,
428
- extensions: userExtensions = [],
429
- onChange
430
- } = config;
431
- const value = signal(initialValue);
432
- const language = signal(initialLanguage);
433
- const theme = signal(initialTheme);
434
- const readOnly = signal(initialReadOnly);
435
- const focused = signal(false);
436
- const view = signal(null);
437
- const docVersion = signal(0);
438
- const languageCompartment = new Compartment();
439
- const themeCompartment = new Compartment();
440
- const readOnlyCompartment = new Compartment();
441
- const extraKeymapCompartment = new Compartment();
442
- const keyModeCompartment = new Compartment();
443
- const cursor = computed(() => {
444
- docVersion();
445
- const v = view.peek();
446
- if (!v) return {
447
- line: 1,
448
- col: 1
449
- };
450
- const pos = v.state.selection.main.head;
451
- const line = v.state.doc.lineAt(pos);
452
- return {
453
- line: line.number,
454
- col: pos - line.from + 1
455
- };
456
- });
457
- const selection = computed(() => {
458
- docVersion();
459
- const v = view.peek();
460
- if (!v) return {
461
- from: 0,
462
- to: 0,
463
- text: ""
464
- };
465
- const sel = v.state.selection.main;
466
- return {
467
- from: sel.from,
468
- to: sel.to,
469
- text: v.state.sliceDoc(sel.from, sel.to)
470
- };
471
- });
472
- const lineCount = computed(() => {
473
- docVersion();
474
- const v = view.peek();
475
- return v ? v.state.doc.lines : initialValue.split("\n").length;
476
- });
477
- const lineHighlights = /* @__PURE__ */new Map();
478
- const lineHighlightField = ViewPlugin.fromClass(class {
479
- decorations;
480
- constructor(editorView) {
481
- this.decorations = this.buildDecos(editorView);
482
- }
483
- buildDecos(editorView) {
484
- const ranges = [];
485
- for (const [lineNum, cls] of lineHighlights) if (lineNum >= 1 && lineNum <= editorView.state.doc.lines) {
486
- const lineInfo = editorView.state.doc.line(lineNum);
487
- ranges.push({
488
- from: lineInfo.from,
489
- deco: Decoration.line({
490
- class: cls
491
- })
492
- });
493
- }
494
- return Decoration.set(ranges.sort((a, b) => a.from - b.from).map(d => d.deco.range(d.from)));
495
- }
496
- update(upd) {
497
- if (upd.docChanged || upd.viewportChanged) this.decorations = this.buildDecos(upd.view);
498
- }
499
- }, {
500
- decorations: plugin => plugin.decorations
501
- });
502
- const gutterMarkers = /* @__PURE__ */new Map();
503
- class CustomGutterMarker extends GutterMarker {
504
- markerText;
505
- markerTitle;
506
- markerClass;
507
- constructor(opts) {
508
- super();
509
- this.markerText = opts.text ?? "";
510
- this.markerTitle = opts.title ?? "";
511
- this.markerClass = opts.class ?? "";
512
- }
513
- toDOM() {
514
- const el = document.createElement("span");
515
- el.textContent = this.markerText;
516
- el.title = this.markerTitle;
517
- if (this.markerClass) el.className = this.markerClass;
518
- el.style.cssText = "cursor: pointer; display: inline-block; width: 100%; text-align: center;";
519
- return el;
520
- }
521
- }
522
- const gutterMarkerExtension = gutter({
523
- class: "pyreon-code-gutter-markers",
524
- lineMarker: (gutterView, line) => {
525
- const lineNo = gutterView.state.doc.lineAt(line.from).number;
526
- const marker = gutterMarkers.get(lineNo);
527
- if (!marker) return null;
528
- return new CustomGutterMarker(marker);
529
- },
530
- initialSpacer: () => new CustomGutterMarker({
531
- text: " "
532
- })
533
- });
534
- function buildExtensions(langExt) {
535
- const exts = [history(), drawSelection(), dropCursor(), rectangularSelection(), crosshairCursor(), highlightActiveLine(), highlightActiveLineGutter(), highlightSelectionMatches(), indentOnInput(), syntaxHighlighting(defaultHighlightStyle, {
536
- fallback: true
537
- }), indentUnit.of(" ".repeat(configTabSize)), keymap.of([...closeBracketsKeymap, ...defaultKeymap, ...searchKeymap, ...historyKeymap, ...foldKeymap, ...completionKeymap, ...lintKeymap, indentWithTab]), languageCompartment.of(langExt), themeCompartment.of(resolveTheme(initialTheme)), readOnlyCompartment.of(EditorState.readOnly.of(initialReadOnly)), extraKeymapCompartment.of([]), keyModeCompartment.of([]), EditorView.updateListener.of(update => {
538
- if (update.docChanged) {
539
- const newValue = update.state.doc.toString();
540
- if (newValue !== value.peek()) {
541
- value.set(newValue);
542
- onChange?.(newValue);
543
- }
544
- docVersion.update(v => v + 1);
545
- }
546
- if (update.selectionSet) docVersion.update(v => v + 1);
547
- if (update.focusChanged) focused.set(update.view.hasFocus);
548
- })];
549
- if (showLineNumbers) exts.push(lineNumbers());
550
- if (showFoldGutter) exts.push(foldGutter());
551
- if (enableBracketMatching) exts.push(bracketMatching(), closeBrackets());
552
- if (enableAutocomplete) exts.push(autocompletion());
553
- if (enableLineWrapping) exts.push(EditorView.lineWrapping);
554
- if (enableIndentGuides) exts.push(EditorView.theme({
555
- ".cm-line": {
556
- backgroundImage: "linear-gradient(to right, #e5e7eb 1px, transparent 1px)",
557
- backgroundSize: `${configTabSize}ch 100%`,
558
- backgroundPosition: "0 0"
559
- }
560
- }));
561
- if (placeholderText) exts.push(placeholder(placeholderText));
562
- if (enableMinimap) exts.push(minimapExtension());
563
- exts.push(lineHighlightField);
564
- exts.push(gutterMarkerExtension);
565
- exts.push(...userExtensions);
566
- return exts;
567
- }
568
- let mounted = false;
569
- async function mount(parent) {
570
- if (mounted) return;
571
- const extensions = buildExtensions(await loadLanguage(language.peek()));
572
- const editorView = new EditorView({
573
- state: EditorState.create({
574
- doc: value.peek(),
575
- extensions
576
- }),
577
- parent
578
- });
579
- view.set(editorView);
580
- mounted = true;
581
- effect(() => {
582
- const val = value();
583
- const v = view.peek();
584
- if (!v) return;
585
- const current = v.state.doc.toString();
586
- if (val !== current) v.dispatch({
587
- changes: {
588
- from: 0,
589
- to: current.length,
590
- insert: val
591
- }
592
- });
593
- });
594
- effect(() => {
595
- const lang = language();
596
- const v = view.peek();
597
- if (!v) return;
598
- loadLanguage(lang).then(ext => {
599
- v.dispatch({
600
- effects: languageCompartment.reconfigure(ext)
601
- });
602
- });
603
- });
604
- effect(() => {
605
- const t = theme();
606
- const v = view.peek();
607
- if (!v) return;
608
- v.dispatch({
609
- effects: themeCompartment.reconfigure(resolveTheme(t))
610
- });
611
- });
612
- effect(() => {
613
- const ro = readOnly();
614
- const v = view.peek();
615
- if (!v) return;
616
- v.dispatch({
617
- effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(ro))
618
- });
619
- });
620
- }
621
- function focus() {
622
- view.peek()?.focus();
623
- }
624
- function insert(text) {
625
- const v = view.peek();
626
- if (!v) return;
627
- const pos = v.state.selection.main.head;
628
- v.dispatch({
629
- changes: {
630
- from: pos,
631
- insert: text
632
- }
633
- });
634
- }
635
- function replaceSelection(text) {
636
- const v = view.peek();
637
- if (!v) return;
638
- v.dispatch(v.state.replaceSelection(text));
639
- }
640
- function select(from, to) {
641
- const v = view.peek();
642
- if (!v) return;
643
- v.dispatch({
644
- selection: {
645
- anchor: from,
646
- head: to
647
- }
648
- });
649
- }
650
- function selectAll() {
651
- const v = view.peek();
652
- if (!v) return;
653
- v.dispatch({
654
- selection: {
655
- anchor: 0,
656
- head: v.state.doc.length
657
- }
658
- });
659
- }
660
- function goToLine(line) {
661
- const v = view.peek();
662
- if (!v) return;
663
- const lineInfo = v.state.doc.line(Math.min(Math.max(1, line), v.state.doc.lines));
664
- v.dispatch({
665
- selection: {
666
- anchor: lineInfo.from
667
- },
668
- scrollIntoView: true
669
- });
670
- v.focus();
671
- }
672
- function undo$1() {
673
- const v = view.peek();
674
- if (v) undo(v);
675
- }
676
- function redo$1() {
677
- const v = view.peek();
678
- if (v) redo(v);
679
- }
680
- function foldAll() {
681
- const v = view.peek();
682
- if (!v) return;
683
- const {
684
- foldAll: foldAllCmd
685
- } = __require("@codemirror/language");
686
- foldAllCmd(v);
687
- }
688
- function unfoldAll() {
689
- const v = view.peek();
690
- if (!v) return;
691
- const {
692
- unfoldAll: unfoldAllCmd
693
- } = __require("@codemirror/language");
694
- unfoldAllCmd(v);
695
- }
696
- function setDiagnostics$1(diagnostics) {
697
- const v = view.peek();
698
- if (!v) return;
699
- v.dispatch(setDiagnostics(v.state, diagnostics.map(d => ({
700
- from: d.from,
701
- to: d.to,
702
- severity: d.severity === "hint" ? "info" : d.severity,
703
- message: d.message,
704
- source: d.source
705
- }))));
706
- }
707
- function clearDiagnostics() {
708
- const v = view.peek();
709
- if (!v) return;
710
- v.dispatch(setDiagnostics(v.state, []));
711
- }
712
- function highlightLine(line, className) {
713
- lineHighlights.set(line, className);
714
- const v = view.peek();
715
- if (v) v.dispatch({
716
- effects: []
717
- });
718
- }
719
- function clearLineHighlights() {
720
- lineHighlights.clear();
721
- const v = view.peek();
722
- if (v) v.dispatch({
723
- effects: []
724
- });
725
- }
726
- function setGutterMarker(line, marker) {
727
- gutterMarkers.set(line, marker);
728
- const v = view.peek();
729
- if (v) v.dispatch({
730
- effects: []
731
- });
732
- }
733
- function clearGutterMarkers() {
734
- gutterMarkers.clear();
735
- const v = view.peek();
736
- if (v) v.dispatch({
737
- effects: []
738
- });
739
- }
740
- const customKeybindings = [];
741
- function addKeybinding(key, handler) {
742
- customKeybindings.push({
743
- key,
744
- run: () => {
745
- handler();
746
- return true;
747
- }
748
- });
749
- const v = view.peek();
750
- if (!v) return;
751
- v.dispatch({
752
- effects: extraKeymapCompartment.reconfigure(keymap.of(customKeybindings))
753
- });
754
- }
755
- function getLine(line) {
756
- const v = view.peek();
757
- if (!v) return "";
758
- const clamped = Math.min(Math.max(1, line), v.state.doc.lines);
759
- return v.state.doc.line(clamped).text;
760
- }
761
- function getWordAtCursor() {
762
- const v = view.peek();
763
- if (!v) return "";
764
- const pos = v.state.selection.main.head;
765
- const line = v.state.doc.lineAt(pos);
766
- const col = pos - line.from;
767
- const text = line.text;
768
- let start = col;
769
- let end = col;
770
- while (start > 0 && /\w/.test(text[start - 1])) start--;
771
- while (end < text.length && /\w/.test(text[end])) end++;
772
- return text.slice(start, end);
773
- }
774
- function scrollTo(pos) {
775
- const v = view.peek();
776
- if (!v) return;
777
- v.dispatch({
778
- effects: EditorView.scrollIntoView(pos, {
779
- y: "center"
780
- })
781
- });
782
- }
783
- async function loadKeyMode() {
784
- const v = view.peek();
785
- if (!v) return;
786
- const vimPkg = "@replit/codemirror-vim";
787
- const emacsPkg = "@replit/codemirror-emacs";
788
- if (enableVim) try {
789
- const mod = await import(/* @vite-ignore */
790
- vimPkg);
791
- v.dispatch({
792
- effects: keyModeCompartment.reconfigure(mod.vim())
793
- });
794
- } catch {}
795
- if (enableEmacs) try {
796
- const mod = await import(/* @vite-ignore */
797
- emacsPkg);
798
- v.dispatch({
799
- effects: keyModeCompartment.reconfigure(mod.emacs())
800
- });
801
- } catch {}
802
- }
803
- function dispose() {
804
- const v = view.peek();
805
- if (v) {
806
- v.destroy();
807
- view.set(null);
808
- mounted = false;
809
- }
810
- }
811
- return {
812
- value,
813
- language,
814
- theme,
815
- readOnly,
816
- cursor,
817
- selection,
818
- lineCount,
819
- focused,
820
- view,
821
- focus,
822
- insert,
823
- replaceSelection,
824
- select,
825
- selectAll,
826
- goToLine,
827
- undo: undo$1,
828
- redo: redo$1,
829
- foldAll,
830
- unfoldAll,
831
- setDiagnostics: setDiagnostics$1,
832
- clearDiagnostics,
833
- highlightLine,
834
- clearLineHighlights,
835
- setGutterMarker,
836
- clearGutterMarkers,
837
- addKeybinding,
838
- getLine,
839
- getWordAtCursor,
840
- scrollTo,
841
- config,
842
- dispose,
843
- _mount: async parent => {
844
- await mount(parent);
845
- await loadKeyMode();
846
- }
847
- };
848
- }
849
-
321
+ * CodeMirror 6 minimap extension.
322
+ * Renders a canvas-based code overview on the right side of the editor.
323
+ *
324
+ * @example
325
+ * ```ts
326
+ * import { minimapExtension } from '@pyreon/code'
327
+ * // Add to editor extensions
328
+ * ```
329
+ */
330
+ declare function minimapExtension(): Extension;
331
+ //#endregion
332
+ //#region src/themes.d.ts
333
+ /**
334
+ * Light theme — clean, minimal.
335
+ */
336
+ declare const lightTheme: Extension;
337
+ /**
338
+ * Dark theme — VS Code inspired.
339
+ */
340
+ declare const darkTheme: Extension;
341
+ /**
342
+ * Resolve a theme value to a CodeMirror extension.
343
+ */
344
+ declare function resolveTheme(theme: EditorTheme): Extension;
850
345
  //#endregion
851
- export { CodeEditor, DiffEditor, TabbedEditor, createEditor, darkTheme, getAvailableLanguages, lightTheme, loadLanguage, minimapExtension, resolveTheme, __exportAll as t };
852
- //# sourceMappingURL=index.d.ts.map
346
+ export { CodeEditor, type CodeEditorProps, DiffEditor, type DiffEditorProps, type EditorConfig, type EditorInstance, type EditorLanguage, type EditorTheme, type GutterMarker, type Tab, TabbedEditor, type TabbedEditorConfig, type TabbedEditorInstance, type TabbedEditorProps, createEditor, darkTheme, getAvailableLanguages, lightTheme, loadLanguage, minimapExtension, resolveTheme };
347
+ //# sourceMappingURL=index2.d.ts.map