@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
@@ -0,0 +1,77 @@
1
+ import type { Extension } from '@codemirror/state'
2
+ import type { EditorLanguage } from './types'
3
+
4
+ /**
5
+ * Language extension loaders — lazy-loaded on demand.
6
+ * Only the requested language is imported, keeping the initial bundle small.
7
+ */
8
+ const languageLoaders: Record<EditorLanguage, () => Promise<Extension>> = {
9
+ javascript: () =>
10
+ import('@codemirror/lang-javascript').then((m) => m.javascript()),
11
+ typescript: () =>
12
+ import('@codemirror/lang-javascript').then((m) =>
13
+ m.javascript({ typescript: true }),
14
+ ),
15
+ jsx: () =>
16
+ import('@codemirror/lang-javascript').then((m) =>
17
+ m.javascript({ jsx: true }),
18
+ ),
19
+ tsx: () =>
20
+ import('@codemirror/lang-javascript').then((m) =>
21
+ m.javascript({ typescript: true, jsx: true }),
22
+ ),
23
+ html: () => import('@codemirror/lang-html').then((m) => m.html()),
24
+ css: () => import('@codemirror/lang-css').then((m) => m.css()),
25
+ json: () => import('@codemirror/lang-json').then((m) => m.json()),
26
+ markdown: () => import('@codemirror/lang-markdown').then((m) => m.markdown()),
27
+ python: () => import('@codemirror/lang-python').then((m) => m.python()),
28
+ rust: () => import('@codemirror/lang-rust').then((m) => m.rust()),
29
+ sql: () => import('@codemirror/lang-sql').then((m) => m.sql()),
30
+ xml: () => import('@codemirror/lang-xml').then((m) => m.xml()),
31
+ yaml: () => import('@codemirror/lang-yaml').then((m) => m.yaml()),
32
+ cpp: () => import('@codemirror/lang-cpp').then((m) => m.cpp()),
33
+ java: () => import('@codemirror/lang-java').then((m) => m.java()),
34
+ go: () => import('@codemirror/lang-go').then((m) => m.go()),
35
+ php: () => import('@codemirror/lang-php').then((m) => m.php()),
36
+ ruby: () => Promise.resolve([]),
37
+ shell: () => Promise.resolve([]),
38
+ plain: () => Promise.resolve([]),
39
+ }
40
+
41
+ // Cache loaded language extensions
42
+ const loaded = new Map<EditorLanguage, Extension>()
43
+
44
+ /**
45
+ * Load a language extension. Returns cached if already loaded.
46
+ * Language grammars are lazy-imported — zero cost until used.
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * const ext = await loadLanguage('typescript')
51
+ * ```
52
+ */
53
+ export async function loadLanguage(
54
+ language: EditorLanguage,
55
+ ): Promise<Extension> {
56
+ const cached = loaded.get(language)
57
+ if (cached) return cached
58
+
59
+ const loader = languageLoaders[language]
60
+ if (!loader) return []
61
+
62
+ try {
63
+ const ext = await loader()
64
+ loaded.set(language, ext)
65
+ return ext
66
+ } catch {
67
+ // Language package not installed — return empty extension
68
+ return []
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Get available languages.
74
+ */
75
+ export function getAvailableLanguages(): EditorLanguage[] {
76
+ return Object.keys(languageLoaders) as EditorLanguage[]
77
+ }
package/src/minimap.ts ADDED
@@ -0,0 +1,160 @@
1
+ import type { Extension } from '@codemirror/state'
2
+ import { EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view'
3
+
4
+ /**
5
+ * Canvas-based minimap extension for CodeMirror 6.
6
+ * Renders a scaled-down overview of the document on the right side.
7
+ */
8
+
9
+ const MINIMAP_WIDTH = 80
10
+ const CHAR_WIDTH = 1.2
11
+ const LINE_HEIGHT = 2.5
12
+ const MINIMAP_BG = '#1e1e2e'
13
+ const MINIMAP_BG_LIGHT = '#f8fafc'
14
+ const TEXT_COLOR = '#585b70'
15
+ const TEXT_COLOR_LIGHT = '#94a3b8'
16
+ const VIEWPORT_COLOR = 'rgba(59, 130, 246, 0.15)'
17
+ const VIEWPORT_BORDER = 'rgba(59, 130, 246, 0.4)'
18
+
19
+ function createMinimapCanvas(): HTMLCanvasElement {
20
+ const canvas = document.createElement('canvas')
21
+ canvas.style.cssText = `position: absolute; right: 0; top: 0; width: ${MINIMAP_WIDTH}px; height: 100%; cursor: pointer; z-index: 5;`
22
+ canvas.width = MINIMAP_WIDTH * 2 // retina
23
+ return canvas
24
+ }
25
+
26
+ function renderMinimap(canvas: HTMLCanvasElement, view: EditorView): void {
27
+ const ctx = canvas.getContext('2d')
28
+ if (!ctx) return
29
+
30
+ const doc = view.state.doc
31
+ const totalLines = doc.lines
32
+ const height = canvas.clientHeight
33
+ canvas.height = height * 2 // retina
34
+
35
+ const isDark = view.dom.classList.contains('cm-dark')
36
+ const bg = isDark ? MINIMAP_BG : MINIMAP_BG_LIGHT
37
+ const textColor = isDark ? TEXT_COLOR : TEXT_COLOR_LIGHT
38
+
39
+ const scale = 2 // retina
40
+ ctx.setTransform(scale, 0, 0, scale, 0, 0)
41
+
42
+ // Background
43
+ ctx.fillStyle = bg
44
+ ctx.fillRect(0, 0, MINIMAP_WIDTH, height)
45
+
46
+ // Calculate visible range in minimap
47
+ const contentHeight = totalLines * LINE_HEIGHT
48
+ const scrollFraction =
49
+ contentHeight > height
50
+ ? view.scrollDOM.scrollTop /
51
+ (view.scrollDOM.scrollHeight - view.scrollDOM.clientHeight || 1)
52
+ : 0
53
+ const offset =
54
+ contentHeight > height ? scrollFraction * (contentHeight - height) : 0
55
+
56
+ // Render text lines
57
+ ctx.fillStyle = textColor
58
+ const startLine = Math.max(1, Math.floor(offset / LINE_HEIGHT))
59
+ const endLine = Math.min(
60
+ totalLines,
61
+ startLine + Math.ceil(height / LINE_HEIGHT) + 1,
62
+ )
63
+
64
+ for (let i = startLine; i <= endLine; i++) {
65
+ const line = doc.line(i)
66
+ const y = (i - 1) * LINE_HEIGHT - offset
67
+ if (y < -LINE_HEIGHT || y > height) continue
68
+
69
+ const text = line.text
70
+ let x = 4
71
+ for (let j = 0; j < Math.min(text.length, 60); j++) {
72
+ if (text[j] !== ' ' && text[j] !== '\t') {
73
+ ctx.fillRect(x, y, CHAR_WIDTH, 1.5)
74
+ }
75
+ x += CHAR_WIDTH
76
+ }
77
+ }
78
+
79
+ // Viewport indicator
80
+ const viewportTop = view.scrollDOM.scrollTop
81
+ const viewportHeight = view.scrollDOM.clientHeight
82
+ const docHeight = view.scrollDOM.scrollHeight || 1
83
+
84
+ const vpY = (viewportTop / docHeight) * Math.min(contentHeight, height)
85
+ const vpH = (viewportHeight / docHeight) * Math.min(contentHeight, height)
86
+
87
+ ctx.fillStyle = VIEWPORT_COLOR
88
+ ctx.fillRect(0, vpY, MINIMAP_WIDTH, vpH)
89
+ ctx.strokeStyle = VIEWPORT_BORDER
90
+ ctx.lineWidth = 1
91
+ ctx.strokeRect(0.5, vpY + 0.5, MINIMAP_WIDTH - 1, vpH - 1)
92
+ }
93
+
94
+ /**
95
+ * CodeMirror 6 minimap extension.
96
+ * Renders a canvas-based code overview on the right side of the editor.
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * import { minimapExtension } from '@pyreon/code'
101
+ * // Add to editor extensions
102
+ * ```
103
+ */
104
+ export function minimapExtension(): Extension {
105
+ return [
106
+ ViewPlugin.fromClass(
107
+ class {
108
+ canvas: HTMLCanvasElement
109
+ view: EditorView
110
+ animFrame: number | null = null
111
+
112
+ constructor(view: EditorView) {
113
+ this.view = view
114
+ this.canvas = createMinimapCanvas()
115
+ view.dom.style.position = 'relative'
116
+ view.dom.appendChild(this.canvas)
117
+
118
+ // Click to scroll
119
+ this.canvas.addEventListener('click', (e) => {
120
+ const rect = this.canvas.getBoundingClientRect()
121
+ const clickY = e.clientY - rect.top
122
+ const fraction = clickY / rect.height
123
+ const scrollTarget =
124
+ fraction *
125
+ (view.scrollDOM.scrollHeight - view.scrollDOM.clientHeight)
126
+ view.scrollDOM.scrollTo({ top: scrollTarget, behavior: 'smooth' })
127
+ })
128
+
129
+ this.render()
130
+ }
131
+
132
+ render() {
133
+ renderMinimap(this.canvas, this.view)
134
+ }
135
+
136
+ update(update: ViewUpdate) {
137
+ if (
138
+ update.docChanged ||
139
+ update.viewportChanged ||
140
+ update.geometryChanged
141
+ ) {
142
+ if (this.animFrame) cancelAnimationFrame(this.animFrame)
143
+ this.animFrame = requestAnimationFrame(() => this.render())
144
+ }
145
+ }
146
+
147
+ destroy() {
148
+ if (this.animFrame) cancelAnimationFrame(this.animFrame)
149
+ this.canvas.remove()
150
+ }
151
+ },
152
+ ),
153
+ // Add padding on the right for the minimap
154
+ EditorView.theme({
155
+ '.cm-scroller': {
156
+ paddingRight: `${MINIMAP_WIDTH + 8}px`,
157
+ },
158
+ }),
159
+ ]
160
+ }
@@ -0,0 +1,231 @@
1
+ import { computed, signal } from '@pyreon/reactivity'
2
+ import { createEditor } from './editor'
3
+ import type {
4
+ EditorLanguage,
5
+ Tab,
6
+ TabbedEditorConfig,
7
+ TabbedEditorInstance,
8
+ } from './types'
9
+
10
+ /**
11
+ * Create a tabbed code editor — multiple files with tab management.
12
+ *
13
+ * Wraps `createEditor()` with tab state. Switching tabs saves the current
14
+ * tab's content and restores the target tab's content/language.
15
+ *
16
+ * @param config - Tabbed editor configuration
17
+ * @returns A TabbedEditorInstance with tab management + underlying editor
18
+ *
19
+ * @example
20
+ * ```tsx
21
+ * const editor = createTabbedEditor({
22
+ * tabs: [
23
+ * { name: 'index.ts', language: 'typescript', value: 'const x = 1' },
24
+ * { name: 'style.css', language: 'css', value: '.app { }' },
25
+ * ],
26
+ * theme: 'dark',
27
+ * })
28
+ *
29
+ * editor.activeTab() // current tab
30
+ * editor.switchTab('style.css')
31
+ * editor.openTab({ name: 'utils.ts', language: 'typescript', value: '' })
32
+ * editor.closeTab('style.css')
33
+ *
34
+ * <TabbedEditor instance={editor} />
35
+ * ```
36
+ */
37
+ export function createTabbedEditor(
38
+ config: TabbedEditorConfig = {},
39
+ ): TabbedEditorInstance {
40
+ const { tabs: initialTabs = [], theme, editorConfig = {} } = config
41
+
42
+ // Ensure all tabs have IDs
43
+ const tabsWithIds = initialTabs.map((t) => ({
44
+ ...t,
45
+ id: t.id ?? t.name,
46
+ closable: t.closable ?? true,
47
+ }))
48
+
49
+ // ── State ──────────────────────────────────────────────────────────────
50
+
51
+ const tabs = signal<Tab[]>(tabsWithIds)
52
+ const activeTabId = signal(tabsWithIds[0]?.id ?? '')
53
+
54
+ // Content cache — stores each tab's current content
55
+ const contentCache = new Map<string, string>()
56
+ for (const tab of tabsWithIds) {
57
+ contentCache.set(tab.id!, tab.value)
58
+ }
59
+
60
+ // ── Editor instance ────────────────────────────────────────────────────
61
+
62
+ const firstTab = tabsWithIds[0]
63
+ const editor = createEditor({
64
+ value: firstTab?.value ?? '',
65
+ language: (firstTab?.language ?? 'plain') as EditorLanguage,
66
+ theme,
67
+ ...editorConfig,
68
+ onChange: (value) => {
69
+ // Save content to cache and mark as modified
70
+ const id = activeTabId.peek()
71
+ if (id) {
72
+ contentCache.set(id, value)
73
+ const originalTab = tabsWithIds.find((t) => t.id === id)
74
+ if (originalTab && value !== originalTab.value) {
75
+ setModified(id, true)
76
+ }
77
+ }
78
+ editorConfig.onChange?.(value)
79
+ },
80
+ })
81
+
82
+ // ── Computed ───────────────────────────────────────────────────────────
83
+
84
+ const activeTab = computed(() => {
85
+ const id = activeTabId()
86
+ return tabs().find((t) => (t.id ?? t.name) === id) ?? null
87
+ })
88
+
89
+ // ── Tab operations ─────────────────────────────────────────────────────
90
+
91
+ function saveCurrentTab(): void {
92
+ const id = activeTabId.peek()
93
+ if (id) {
94
+ contentCache.set(id, editor.value.peek())
95
+ }
96
+ }
97
+
98
+ function switchTab(id: string): void {
99
+ const tab = tabs.peek().find((t) => (t.id ?? t.name) === id)
100
+ if (!tab) return
101
+
102
+ // Save current tab content
103
+ saveCurrentTab()
104
+
105
+ // Switch
106
+ activeTabId.set(id)
107
+
108
+ // Restore target tab content
109
+ const cached = contentCache.get(id)
110
+ editor.value.set(cached ?? tab.value)
111
+ editor.language.set((tab.language ?? 'plain') as EditorLanguage)
112
+ }
113
+
114
+ function openTab(tab: Tab): void {
115
+ const id = tab.id ?? tab.name
116
+ const existing = tabs.peek().find((t) => (t.id ?? t.name) === id)
117
+
118
+ if (existing) {
119
+ // Already open — just switch to it
120
+ switchTab(id)
121
+ return
122
+ }
123
+
124
+ const newTab = { ...tab, id, closable: tab.closable ?? true }
125
+ tabs.update((t) => [...t, newTab])
126
+ contentCache.set(id, tab.value)
127
+ switchTab(id)
128
+ }
129
+
130
+ function closeTab(id: string): void {
131
+ const currentTabs = tabs.peek()
132
+ const tabIndex = currentTabs.findIndex((t) => (t.id ?? t.name) === id)
133
+ if (tabIndex === -1) return
134
+
135
+ const tab = currentTabs[tabIndex]!
136
+ if (tab.closable === false) return
137
+
138
+ // Remove from state
139
+ tabs.update((t) => t.filter((item) => (item.id ?? item.name) !== id))
140
+ contentCache.delete(id)
141
+
142
+ // If closing the active tab, switch to adjacent
143
+ if (activeTabId.peek() === id) {
144
+ const remaining = tabs.peek()
145
+ if (remaining.length > 0) {
146
+ const nextIndex = Math.min(tabIndex, remaining.length - 1)
147
+ switchTab(remaining[nextIndex]!.id ?? remaining[nextIndex]!.name)
148
+ } else {
149
+ activeTabId.set('')
150
+ editor.value.set('')
151
+ }
152
+ }
153
+ }
154
+
155
+ function renameTab(id: string, name: string): void {
156
+ tabs.update((t) =>
157
+ t.map((tab) => ((tab.id ?? tab.name) === id ? { ...tab, name } : tab)),
158
+ )
159
+ }
160
+
161
+ function setModified(id: string, modified: boolean): void {
162
+ tabs.update((t) =>
163
+ t.map((tab) =>
164
+ (tab.id ?? tab.name) === id ? { ...tab, modified } : tab,
165
+ ),
166
+ )
167
+ }
168
+
169
+ function moveTab(fromIndex: number, toIndex: number): void {
170
+ tabs.update((t) => {
171
+ const arr = [...t]
172
+ const [moved] = arr.splice(fromIndex, 1)
173
+ if (moved) arr.splice(toIndex, 0, moved)
174
+ return arr
175
+ })
176
+ }
177
+
178
+ function getTab(id: string): Tab | undefined {
179
+ return tabs.peek().find((t) => (t.id ?? t.name) === id)
180
+ }
181
+
182
+ function closeAll(): void {
183
+ const closable = tabs.peek().filter((t) => t.closable !== false)
184
+ for (const tab of closable) {
185
+ contentCache.delete(tab.id ?? tab.name)
186
+ }
187
+ tabs.update((t) => t.filter((tab) => tab.closable === false))
188
+ const remaining = tabs.peek()
189
+ if (remaining.length > 0) {
190
+ switchTab(remaining[0]!.id ?? remaining[0]!.name)
191
+ } else {
192
+ activeTabId.set('')
193
+ editor.value.set('')
194
+ }
195
+ }
196
+
197
+ function closeOthers(id: string): void {
198
+ const toClose = tabs
199
+ .peek()
200
+ .filter((t) => (t.id ?? t.name) !== id && t.closable !== false)
201
+ for (const tab of toClose) {
202
+ contentCache.delete(tab.id ?? tab.name)
203
+ }
204
+ tabs.update((t) =>
205
+ t.filter((tab) => (tab.id ?? tab.name) === id || tab.closable === false),
206
+ )
207
+ switchTab(id)
208
+ }
209
+
210
+ function dispose(): void {
211
+ contentCache.clear()
212
+ editor.dispose()
213
+ }
214
+
215
+ return {
216
+ editor,
217
+ tabs,
218
+ activeTab,
219
+ activeTabId,
220
+ openTab,
221
+ closeTab,
222
+ switchTab,
223
+ renameTab,
224
+ setModified,
225
+ moveTab,
226
+ getTab,
227
+ closeAll,
228
+ closeOthers,
229
+ dispose,
230
+ }
231
+ }