@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
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
import { effect } from '@pyreon/reactivity'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { createEditor } from '../editor'
|
|
4
|
+
import { getAvailableLanguages } from '../languages'
|
|
5
|
+
import { createTabbedEditor } from '../tabbed-editor'
|
|
6
|
+
|
|
7
|
+
describe('createEditor', () => {
|
|
8
|
+
describe('initialization', () => {
|
|
9
|
+
it('creates with default values', () => {
|
|
10
|
+
const editor = createEditor()
|
|
11
|
+
expect(editor.value()).toBe('')
|
|
12
|
+
expect(editor.language()).toBe('plain')
|
|
13
|
+
expect(editor.readOnly()).toBe(false)
|
|
14
|
+
expect(editor.focused()).toBe(false)
|
|
15
|
+
expect(editor.view()).toBeNull()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('creates with initial value', () => {
|
|
19
|
+
const editor = createEditor({ value: 'hello world' })
|
|
20
|
+
expect(editor.value()).toBe('hello world')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('creates with language', () => {
|
|
24
|
+
const editor = createEditor({ language: 'typescript' })
|
|
25
|
+
expect(editor.language()).toBe('typescript')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('creates with theme', () => {
|
|
29
|
+
const editor = createEditor({ theme: 'dark' })
|
|
30
|
+
expect(editor.theme()).toBe('dark')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('creates with readOnly', () => {
|
|
34
|
+
const editor = createEditor({ readOnly: true })
|
|
35
|
+
expect(editor.readOnly()).toBe(true)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('stores config', () => {
|
|
39
|
+
const config = {
|
|
40
|
+
value: 'test',
|
|
41
|
+
language: 'json' as const,
|
|
42
|
+
theme: 'dark' as const,
|
|
43
|
+
lineNumbers: true,
|
|
44
|
+
tabSize: 4,
|
|
45
|
+
}
|
|
46
|
+
const editor = createEditor(config)
|
|
47
|
+
expect(editor.config).toBe(config)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
describe('signal reactivity', () => {
|
|
52
|
+
it('value is a writable signal', () => {
|
|
53
|
+
const editor = createEditor({ value: 'initial' })
|
|
54
|
+
expect(editor.value()).toBe('initial')
|
|
55
|
+
|
|
56
|
+
editor.value.set('updated')
|
|
57
|
+
expect(editor.value()).toBe('updated')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('language is a writable signal', () => {
|
|
61
|
+
const editor = createEditor({ language: 'javascript' })
|
|
62
|
+
editor.language.set('python')
|
|
63
|
+
expect(editor.language()).toBe('python')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('theme is a writable signal', () => {
|
|
67
|
+
const editor = createEditor({ theme: 'light' })
|
|
68
|
+
editor.theme.set('dark')
|
|
69
|
+
expect(editor.theme()).toBe('dark')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('readOnly is a writable signal', () => {
|
|
73
|
+
const editor = createEditor({ readOnly: false })
|
|
74
|
+
editor.readOnly.set(true)
|
|
75
|
+
expect(editor.readOnly()).toBe(true)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('value is reactive in effects', () => {
|
|
79
|
+
const editor = createEditor({ value: 'a' })
|
|
80
|
+
const values: string[] = []
|
|
81
|
+
|
|
82
|
+
effect(() => {
|
|
83
|
+
values.push(editor.value())
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
editor.value.set('b')
|
|
87
|
+
editor.value.set('c')
|
|
88
|
+
|
|
89
|
+
expect(values).toEqual(['a', 'b', 'c'])
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('computed properties (before mount)', () => {
|
|
94
|
+
it('cursor returns default before mount', () => {
|
|
95
|
+
const editor = createEditor()
|
|
96
|
+
expect(editor.cursor()).toEqual({ line: 1, col: 1 })
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('selection returns default before mount', () => {
|
|
100
|
+
const editor = createEditor()
|
|
101
|
+
expect(editor.selection()).toEqual({ from: 0, to: 0, text: '' })
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('lineCount returns initial line count', () => {
|
|
105
|
+
const editor = createEditor({ value: 'line1\nline2\nline3' })
|
|
106
|
+
expect(editor.lineCount()).toBe(3)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('lineCount for single line', () => {
|
|
110
|
+
const editor = createEditor({ value: 'hello' })
|
|
111
|
+
expect(editor.lineCount()).toBe(1)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('lineCount for empty', () => {
|
|
115
|
+
const editor = createEditor({ value: '' })
|
|
116
|
+
expect(editor.lineCount()).toBe(1)
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('actions (before mount)', () => {
|
|
121
|
+
it('focus does not throw before mount', () => {
|
|
122
|
+
const editor = createEditor()
|
|
123
|
+
expect(() => editor.focus()).not.toThrow()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('insert does not throw before mount', () => {
|
|
127
|
+
const editor = createEditor()
|
|
128
|
+
expect(() => editor.insert('text')).not.toThrow()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('replaceSelection does not throw before mount', () => {
|
|
132
|
+
const editor = createEditor()
|
|
133
|
+
expect(() => editor.replaceSelection('text')).not.toThrow()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('select does not throw before mount', () => {
|
|
137
|
+
const editor = createEditor()
|
|
138
|
+
expect(() => editor.select(0, 5)).not.toThrow()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('selectAll does not throw before mount', () => {
|
|
142
|
+
const editor = createEditor()
|
|
143
|
+
expect(() => editor.selectAll()).not.toThrow()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('goToLine does not throw before mount', () => {
|
|
147
|
+
const editor = createEditor()
|
|
148
|
+
expect(() => editor.goToLine(5)).not.toThrow()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('undo does not throw before mount', () => {
|
|
152
|
+
const editor = createEditor()
|
|
153
|
+
expect(() => editor.undo()).not.toThrow()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('redo does not throw before mount', () => {
|
|
157
|
+
const editor = createEditor()
|
|
158
|
+
expect(() => editor.redo()).not.toThrow()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('dispose does not throw before mount', () => {
|
|
162
|
+
const editor = createEditor()
|
|
163
|
+
expect(() => editor.dispose()).not.toThrow()
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('onChange callback', () => {
|
|
168
|
+
it('config stores onChange', () => {
|
|
169
|
+
const onChange = () => {
|
|
170
|
+
/* noop */
|
|
171
|
+
}
|
|
172
|
+
const editor = createEditor({ onChange })
|
|
173
|
+
expect(editor.config.onChange).toBe(onChange)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe('new actions (before mount)', () => {
|
|
178
|
+
it('setDiagnostics does not throw before mount', () => {
|
|
179
|
+
const editor = createEditor()
|
|
180
|
+
expect(() =>
|
|
181
|
+
editor.setDiagnostics([
|
|
182
|
+
{ from: 0, to: 5, severity: 'error', message: 'test' },
|
|
183
|
+
]),
|
|
184
|
+
).not.toThrow()
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('clearDiagnostics does not throw before mount', () => {
|
|
188
|
+
const editor = createEditor()
|
|
189
|
+
expect(() => editor.clearDiagnostics()).not.toThrow()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('highlightLine does not throw before mount', () => {
|
|
193
|
+
const editor = createEditor()
|
|
194
|
+
expect(() => editor.highlightLine(1, 'error-line')).not.toThrow()
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('clearLineHighlights does not throw before mount', () => {
|
|
198
|
+
const editor = createEditor()
|
|
199
|
+
expect(() => editor.clearLineHighlights()).not.toThrow()
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('setGutterMarker does not throw before mount', () => {
|
|
203
|
+
const editor = createEditor()
|
|
204
|
+
expect(() =>
|
|
205
|
+
editor.setGutterMarker(1, { text: '🔴', title: 'Breakpoint' }),
|
|
206
|
+
).not.toThrow()
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('clearGutterMarkers does not throw before mount', () => {
|
|
210
|
+
const editor = createEditor()
|
|
211
|
+
expect(() => editor.clearGutterMarkers()).not.toThrow()
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('addKeybinding does not throw before mount', () => {
|
|
215
|
+
const editor = createEditor()
|
|
216
|
+
expect(() =>
|
|
217
|
+
editor.addKeybinding('Ctrl-Shift-L', () => true),
|
|
218
|
+
).not.toThrow()
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('getLine returns empty string before mount', () => {
|
|
222
|
+
const editor = createEditor()
|
|
223
|
+
expect(editor.getLine(1)).toBe('')
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('getWordAtCursor returns empty string before mount', () => {
|
|
227
|
+
const editor = createEditor()
|
|
228
|
+
expect(editor.getWordAtCursor()).toBe('')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('scrollTo does not throw before mount', () => {
|
|
232
|
+
const editor = createEditor()
|
|
233
|
+
expect(() => editor.scrollTo(0)).not.toThrow()
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
describe('config options', () => {
|
|
238
|
+
it('highlightIndentGuides defaults to true', () => {
|
|
239
|
+
const editor = createEditor()
|
|
240
|
+
expect(editor.config.highlightIndentGuides).toBeUndefined() // uses default
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('vim mode can be enabled', () => {
|
|
244
|
+
const editor = createEditor({ vim: true })
|
|
245
|
+
expect(editor.config.vim).toBe(true)
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('emacs mode can be enabled', () => {
|
|
249
|
+
const editor = createEditor({ emacs: true })
|
|
250
|
+
expect(editor.config.emacs).toBe(true)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('minimap can be enabled', () => {
|
|
254
|
+
const editor = createEditor({ minimap: true })
|
|
255
|
+
expect(editor.config.minimap).toBe(true)
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('all config options are stored', () => {
|
|
259
|
+
const editor = createEditor({
|
|
260
|
+
value: 'test',
|
|
261
|
+
language: 'typescript',
|
|
262
|
+
theme: 'dark',
|
|
263
|
+
lineNumbers: false,
|
|
264
|
+
readOnly: true,
|
|
265
|
+
foldGutter: false,
|
|
266
|
+
bracketMatching: false,
|
|
267
|
+
autocomplete: false,
|
|
268
|
+
search: false,
|
|
269
|
+
lint: true,
|
|
270
|
+
highlightIndentGuides: false,
|
|
271
|
+
tabSize: 4,
|
|
272
|
+
lineWrapping: true,
|
|
273
|
+
placeholder: 'Type here...',
|
|
274
|
+
})
|
|
275
|
+
expect(editor.config.tabSize).toBe(4)
|
|
276
|
+
expect(editor.config.lineWrapping).toBe(true)
|
|
277
|
+
expect(editor.config.placeholder).toBe('Type here...')
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
describe('createTabbedEditor', () => {
|
|
283
|
+
it('creates with initial tabs', () => {
|
|
284
|
+
const te = createTabbedEditor({
|
|
285
|
+
tabs: [
|
|
286
|
+
{ name: 'index.ts', language: 'typescript', value: 'const x = 1' },
|
|
287
|
+
{ name: 'style.css', language: 'css', value: '.app {}' },
|
|
288
|
+
],
|
|
289
|
+
})
|
|
290
|
+
expect(te.tabs()).toHaveLength(2)
|
|
291
|
+
expect(te.activeTabId()).toBe('index.ts')
|
|
292
|
+
expect(te.activeTab()?.name).toBe('index.ts')
|
|
293
|
+
expect(te.editor.value()).toBe('const x = 1')
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
it('creates with no tabs', () => {
|
|
297
|
+
const te = createTabbedEditor()
|
|
298
|
+
expect(te.tabs()).toHaveLength(0)
|
|
299
|
+
expect(te.activeTabId()).toBe('')
|
|
300
|
+
expect(te.activeTab()).toBeNull()
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('switchTab changes active tab and editor content', () => {
|
|
304
|
+
const te = createTabbedEditor({
|
|
305
|
+
tabs: [
|
|
306
|
+
{ name: 'a.ts', value: 'aaa' },
|
|
307
|
+
{ name: 'b.ts', value: 'bbb' },
|
|
308
|
+
],
|
|
309
|
+
})
|
|
310
|
+
expect(te.editor.value()).toBe('aaa')
|
|
311
|
+
|
|
312
|
+
te.switchTab('b.ts')
|
|
313
|
+
expect(te.activeTabId()).toBe('b.ts')
|
|
314
|
+
expect(te.editor.value()).toBe('bbb')
|
|
315
|
+
|
|
316
|
+
te.switchTab('a.ts')
|
|
317
|
+
expect(te.editor.value()).toBe('aaa')
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it('openTab adds and switches to new tab', () => {
|
|
321
|
+
const te = createTabbedEditor({
|
|
322
|
+
tabs: [{ name: 'a.ts', value: 'aaa' }],
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
te.openTab({ name: 'b.ts', language: 'typescript', value: 'bbb' })
|
|
326
|
+
expect(te.tabs()).toHaveLength(2)
|
|
327
|
+
expect(te.activeTabId()).toBe('b.ts')
|
|
328
|
+
expect(te.editor.value()).toBe('bbb')
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('openTab switches to existing tab', () => {
|
|
332
|
+
const te = createTabbedEditor({
|
|
333
|
+
tabs: [
|
|
334
|
+
{ name: 'a.ts', value: 'aaa' },
|
|
335
|
+
{ name: 'b.ts', value: 'bbb' },
|
|
336
|
+
],
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
te.openTab({ name: 'b.ts', value: 'bbb' })
|
|
340
|
+
expect(te.tabs()).toHaveLength(2) // not duplicated
|
|
341
|
+
expect(te.activeTabId()).toBe('b.ts')
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('closeTab removes tab', () => {
|
|
345
|
+
const te = createTabbedEditor({
|
|
346
|
+
tabs: [
|
|
347
|
+
{ name: 'a.ts', value: 'aaa' },
|
|
348
|
+
{ name: 'b.ts', value: 'bbb' },
|
|
349
|
+
],
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
te.closeTab('b.ts')
|
|
353
|
+
expect(te.tabs()).toHaveLength(1)
|
|
354
|
+
expect(te.tabs()[0]!.name).toBe('a.ts')
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('closeTab switches to adjacent when closing active', () => {
|
|
358
|
+
const te = createTabbedEditor({
|
|
359
|
+
tabs: [
|
|
360
|
+
{ name: 'a.ts', value: 'aaa' },
|
|
361
|
+
{ name: 'b.ts', value: 'bbb' },
|
|
362
|
+
{ name: 'c.ts', value: 'ccc' },
|
|
363
|
+
],
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
te.switchTab('b.ts')
|
|
367
|
+
te.closeTab('b.ts')
|
|
368
|
+
// Should switch to c.ts (next) or a.ts
|
|
369
|
+
expect(te.activeTabId()).not.toBe('b.ts')
|
|
370
|
+
expect(te.tabs()).toHaveLength(2)
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('closeTab respects closable: false', () => {
|
|
374
|
+
const te = createTabbedEditor({
|
|
375
|
+
tabs: [{ name: 'main.ts', value: 'main', closable: false }],
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
te.closeTab('main.ts')
|
|
379
|
+
expect(te.tabs()).toHaveLength(1) // not closed
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
it('renameTab changes tab name', () => {
|
|
383
|
+
const te = createTabbedEditor({
|
|
384
|
+
tabs: [{ name: 'old.ts', value: '' }],
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
te.renameTab('old.ts', 'new.ts')
|
|
388
|
+
expect(te.tabs()[0]!.name).toBe('new.ts')
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it('setModified marks tab', () => {
|
|
392
|
+
const te = createTabbedEditor({
|
|
393
|
+
tabs: [{ name: 'a.ts', value: '' }],
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
te.setModified('a.ts', true)
|
|
397
|
+
expect(te.tabs()[0]!.modified).toBe(true)
|
|
398
|
+
|
|
399
|
+
te.setModified('a.ts', false)
|
|
400
|
+
expect(te.tabs()[0]!.modified).toBe(false)
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it('moveTab reorders tabs', () => {
|
|
404
|
+
const te = createTabbedEditor({
|
|
405
|
+
tabs: [
|
|
406
|
+
{ name: 'a.ts', value: '' },
|
|
407
|
+
{ name: 'b.ts', value: '' },
|
|
408
|
+
{ name: 'c.ts', value: '' },
|
|
409
|
+
],
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
te.moveTab(0, 2)
|
|
413
|
+
expect(te.tabs().map((t: any) => t.name)).toEqual(['b.ts', 'c.ts', 'a.ts'])
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
it('getTab returns tab by id', () => {
|
|
417
|
+
const te = createTabbedEditor({
|
|
418
|
+
tabs: [{ name: 'a.ts', value: 'content' }],
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
expect(te.getTab('a.ts')?.value).toBe('content')
|
|
422
|
+
expect(te.getTab('missing')).toBeUndefined()
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('closeAll closes all closable tabs', () => {
|
|
426
|
+
const te = createTabbedEditor({
|
|
427
|
+
tabs: [
|
|
428
|
+
{ name: 'a.ts', value: '', closable: false },
|
|
429
|
+
{ name: 'b.ts', value: '' },
|
|
430
|
+
{ name: 'c.ts', value: '' },
|
|
431
|
+
],
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
te.closeAll()
|
|
435
|
+
expect(te.tabs()).toHaveLength(1)
|
|
436
|
+
expect(te.tabs()[0]!.name).toBe('a.ts')
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it('closeOthers closes all except specified', () => {
|
|
440
|
+
const te = createTabbedEditor({
|
|
441
|
+
tabs: [
|
|
442
|
+
{ name: 'a.ts', value: '' },
|
|
443
|
+
{ name: 'b.ts', value: '' },
|
|
444
|
+
{ name: 'c.ts', value: '' },
|
|
445
|
+
],
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
te.closeOthers('b.ts')
|
|
449
|
+
expect(te.tabs()).toHaveLength(1)
|
|
450
|
+
expect(te.tabs()[0]!.name).toBe('b.ts')
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
it('preserves content when switching tabs', () => {
|
|
454
|
+
const te = createTabbedEditor({
|
|
455
|
+
tabs: [
|
|
456
|
+
{ name: 'a.ts', value: 'original-a' },
|
|
457
|
+
{ name: 'b.ts', value: 'original-b' },
|
|
458
|
+
],
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
// Modify a.ts via signal
|
|
462
|
+
te.editor.value.set('modified-a')
|
|
463
|
+
|
|
464
|
+
// Switch to b.ts
|
|
465
|
+
te.switchTab('b.ts')
|
|
466
|
+
expect(te.editor.value()).toBe('original-b')
|
|
467
|
+
|
|
468
|
+
// Switch back — should have the modified content
|
|
469
|
+
te.switchTab('a.ts')
|
|
470
|
+
expect(te.editor.value()).toBe('modified-a')
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
it('dispose cleans up', () => {
|
|
474
|
+
const te = createTabbedEditor({
|
|
475
|
+
tabs: [{ name: 'a.ts', value: '' }],
|
|
476
|
+
})
|
|
477
|
+
expect(() => te.dispose()).not.toThrow()
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('close last tab clears editor', () => {
|
|
481
|
+
const te = createTabbedEditor({
|
|
482
|
+
tabs: [{ name: 'a.ts', value: 'content' }],
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
te.closeTab('a.ts')
|
|
486
|
+
expect(te.tabs()).toHaveLength(0)
|
|
487
|
+
expect(te.activeTabId()).toBe('')
|
|
488
|
+
expect(te.editor.value()).toBe('')
|
|
489
|
+
})
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
describe('getAvailableLanguages', () => {
|
|
493
|
+
it('returns all supported languages', () => {
|
|
494
|
+
const langs = getAvailableLanguages()
|
|
495
|
+
expect(langs).toContain('javascript')
|
|
496
|
+
expect(langs).toContain('typescript')
|
|
497
|
+
expect(langs).toContain('html')
|
|
498
|
+
expect(langs).toContain('css')
|
|
499
|
+
expect(langs).toContain('json')
|
|
500
|
+
expect(langs).toContain('python')
|
|
501
|
+
expect(langs).toContain('markdown')
|
|
502
|
+
expect(langs).toContain('plain')
|
|
503
|
+
expect(langs.length).toBeGreaterThanOrEqual(15)
|
|
504
|
+
})
|
|
505
|
+
})
|
package/src/themes.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Extension } from '@codemirror/state'
|
|
2
|
+
import { EditorView } from '@codemirror/view'
|
|
3
|
+
import type { EditorTheme } from './types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Light theme — clean, minimal.
|
|
7
|
+
*/
|
|
8
|
+
export const lightTheme: Extension = EditorView.theme({
|
|
9
|
+
'&': {
|
|
10
|
+
backgroundColor: '#ffffff',
|
|
11
|
+
color: '#1e293b',
|
|
12
|
+
},
|
|
13
|
+
'.cm-content': {
|
|
14
|
+
caretColor: '#1e293b',
|
|
15
|
+
},
|
|
16
|
+
'.cm-cursor': {
|
|
17
|
+
borderLeftColor: '#1e293b',
|
|
18
|
+
},
|
|
19
|
+
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground': {
|
|
20
|
+
backgroundColor: '#dbeafe',
|
|
21
|
+
},
|
|
22
|
+
'.cm-gutters': {
|
|
23
|
+
backgroundColor: '#f8fafc',
|
|
24
|
+
color: '#94a3b8',
|
|
25
|
+
borderRight: '1px solid #e2e8f0',
|
|
26
|
+
},
|
|
27
|
+
'.cm-activeLineGutter': {
|
|
28
|
+
backgroundColor: '#f1f5f9',
|
|
29
|
+
color: '#475569',
|
|
30
|
+
},
|
|
31
|
+
'.cm-activeLine': {
|
|
32
|
+
backgroundColor: '#f8fafc',
|
|
33
|
+
},
|
|
34
|
+
'.cm-foldGutter': {
|
|
35
|
+
color: '#94a3b8',
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Dark theme — VS Code inspired.
|
|
41
|
+
*/
|
|
42
|
+
export const darkTheme: Extension = EditorView.theme(
|
|
43
|
+
{
|
|
44
|
+
'&': {
|
|
45
|
+
backgroundColor: '#1e1e2e',
|
|
46
|
+
color: '#cdd6f4',
|
|
47
|
+
},
|
|
48
|
+
'.cm-content': {
|
|
49
|
+
caretColor: '#f5e0dc',
|
|
50
|
+
},
|
|
51
|
+
'.cm-cursor': {
|
|
52
|
+
borderLeftColor: '#f5e0dc',
|
|
53
|
+
},
|
|
54
|
+
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground': {
|
|
55
|
+
backgroundColor: '#45475a',
|
|
56
|
+
},
|
|
57
|
+
'.cm-gutters': {
|
|
58
|
+
backgroundColor: '#181825',
|
|
59
|
+
color: '#585b70',
|
|
60
|
+
borderRight: '1px solid #313244',
|
|
61
|
+
},
|
|
62
|
+
'.cm-activeLineGutter': {
|
|
63
|
+
backgroundColor: '#1e1e2e',
|
|
64
|
+
color: '#a6adc8',
|
|
65
|
+
},
|
|
66
|
+
'.cm-activeLine': {
|
|
67
|
+
backgroundColor: '#1e1e2e80',
|
|
68
|
+
},
|
|
69
|
+
'.cm-foldGutter': {
|
|
70
|
+
color: '#585b70',
|
|
71
|
+
},
|
|
72
|
+
'.cm-matchingBracket': {
|
|
73
|
+
backgroundColor: '#45475a',
|
|
74
|
+
color: '#f5e0dc',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{ dark: true },
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Resolve a theme value to a CodeMirror extension.
|
|
82
|
+
*/
|
|
83
|
+
export function resolveTheme(theme: EditorTheme): Extension {
|
|
84
|
+
if (theme === 'light') return lightTheme
|
|
85
|
+
if (theme === 'dark') return darkTheme
|
|
86
|
+
return theme // custom Extension
|
|
87
|
+
}
|