@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/languages.ts
ADDED
|
@@ -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
|
+
}
|