@joinezco/codeblock 0.0.8 → 0.0.9
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/dist/assets/fs.worker-DfanUHpQ.js +21 -0
- package/dist/assets/{index-MGle_v2x.js → index-BAnLzvMk.js} +1 -1
- package/dist/assets/{index-as7ELo0J.js → index-BBC9WDX6.js} +1 -1
- package/dist/assets/{index-Dx_VuNNd.js → index-BEXYxRro.js} +1 -1
- package/dist/assets/{index-pGm0qkrJ.js → index-BfYmUKH9.js} +1 -1
- package/dist/assets/{index-CXFONXS8.js → index-BhaTNAWE.js} +1 -1
- package/dist/assets/{index-D5Z27j1C.js → index-CCbYDSng.js} +1 -1
- package/dist/assets/{index-Dvu-FFzd.js → index-CIi8tLT6.js} +1 -1
- package/dist/assets/{index-C-QhPFHP.js → index-CaANcgI2.js} +1 -1
- package/dist/assets/index-CkWzFNzm.js +208 -0
- package/dist/assets/{index-N-GE7HTU.js → index-D_XGv9QZ.js} +1 -1
- package/dist/assets/{index-DWOBdRjn.js → index-DkmiPfkD.js} +1 -1
- package/dist/assets/{index-CGx5MZO7.js → index-DmNlLMQ4.js} +1 -1
- package/dist/assets/{index-I0dlv-r3.js → index-DmX_vI7D.js} +1 -1
- package/dist/assets/{index-9HdhmM_Y.js → index-DogEEevD.js} +1 -1
- package/dist/assets/{index-aEsF5o-7.js → index-DsDl5qZV.js} +1 -1
- package/dist/assets/{index-gUUzXNuP.js → index-gAy5mDg-.js} +1 -1
- package/dist/assets/{index-CIuq3uTk.js → index-i5qJLB2h.js} +1 -1
- package/dist/assets/javascript.worker-ClsyHOLi.js +552 -0
- package/dist/e2e/editor.spec.d.ts +1 -0
- package/dist/e2e/editor.spec.js +309 -0
- package/dist/editor.d.ts +9 -3
- package/dist/editor.js +83 -15
- package/dist/index.d.ts +3 -0
- package/dist/index.html +2 -3
- package/dist/index.js +3 -0
- package/dist/lsps/typescript.d.ts +3 -1
- package/dist/lsps/typescript.js +8 -17
- package/dist/panels/footer.d.ts +16 -0
- package/dist/panels/footer.js +258 -0
- package/dist/panels/toolbar.d.ts +15 -2
- package/dist/panels/toolbar.js +528 -115
- package/dist/panels/toolbar.test.js +20 -14
- package/dist/rpc/transport.d.ts +2 -11
- package/dist/rpc/transport.js +19 -35
- package/dist/themes/index.js +181 -14
- package/dist/themes/vscode.js +3 -2
- package/dist/utils/fs.d.ts +15 -3
- package/dist/utils/fs.js +85 -6
- package/dist/utils/lsp.d.ts +26 -15
- package/dist/utils/lsp.js +79 -44
- package/dist/utils/search.d.ts +2 -0
- package/dist/utils/search.js +13 -4
- package/dist/utils/typescript-defaults.d.ts +57 -0
- package/dist/utils/typescript-defaults.js +208 -0
- package/dist/utils/typescript-defaults.test.d.ts +1 -0
- package/dist/utils/typescript-defaults.test.js +197 -0
- package/dist/workers/fs.worker.js +14 -18
- package/dist/workers/javascript.worker.js +11 -9
- package/package.json +4 -3
- package/dist/assets/fs.worker-BwEqZcql.ts +0 -109
- package/dist/assets/index-C3BnE2cG.js +0 -222
- package/dist/assets/javascript.worker-C1zGArKk.js +0 -527
- package/dist/snapshot.bin +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
|
2
|
+
import puppeteer from 'puppeteer-core';
|
|
3
|
+
const BASE_URL = 'http://localhost:5173';
|
|
4
|
+
const CHROME_PATH = '/usr/bin/google-chrome';
|
|
5
|
+
// Helper: wait for the CodeMirror editor to be ready
|
|
6
|
+
async function waitForEditor(page) {
|
|
7
|
+
await page.waitForSelector('.cm-editor', { visible: true });
|
|
8
|
+
await page.waitForSelector('.cm-content', { visible: true });
|
|
9
|
+
}
|
|
10
|
+
// Helper: type into the editor content area
|
|
11
|
+
async function typeInEditor(page, text) {
|
|
12
|
+
await page.click('.cm-content');
|
|
13
|
+
await page.keyboard.type(text);
|
|
14
|
+
}
|
|
15
|
+
// Helper: get the current editor text
|
|
16
|
+
async function getEditorText(page) {
|
|
17
|
+
return page.$eval('.cm-content', el => el.textContent);
|
|
18
|
+
}
|
|
19
|
+
// Helper: get toolbar input value
|
|
20
|
+
async function getToolbarValue(page) {
|
|
21
|
+
return page.$eval('.cm-toolbar-input', (el) => el.value);
|
|
22
|
+
}
|
|
23
|
+
// Helper: create a new file via toolbar
|
|
24
|
+
async function createFile(page, filename) {
|
|
25
|
+
await page.click('.cm-toolbar-input', { count: 3 }); // triple-click to select all
|
|
26
|
+
await page.type('.cm-toolbar-input', filename);
|
|
27
|
+
// Wait for the dropdown to show results
|
|
28
|
+
await page.waitForSelector('.cm-search-result', { timeout: 2000 });
|
|
29
|
+
// Select the create command (first result)
|
|
30
|
+
const createCommand = await page.$('.cm-command-result');
|
|
31
|
+
if (createCommand) {
|
|
32
|
+
await createCommand.click();
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
await page.keyboard.press('Enter');
|
|
36
|
+
}
|
|
37
|
+
// Wait for file to load
|
|
38
|
+
await new Promise(r => setTimeout(r, 500));
|
|
39
|
+
}
|
|
40
|
+
// Helper: open an existing file via toolbar
|
|
41
|
+
async function openFile(page, filename) {
|
|
42
|
+
await page.click('.cm-toolbar-input', { count: 3 });
|
|
43
|
+
await page.type('.cm-toolbar-input', filename);
|
|
44
|
+
await page.waitForSelector('.cm-file-result', { timeout: 3000 });
|
|
45
|
+
await page.click('.cm-file-result');
|
|
46
|
+
await new Promise(r => setTimeout(r, 500));
|
|
47
|
+
}
|
|
48
|
+
describe('Editor - File Operations', () => {
|
|
49
|
+
let browser;
|
|
50
|
+
let page;
|
|
51
|
+
beforeAll(async () => {
|
|
52
|
+
browser = await puppeteer.launch({
|
|
53
|
+
executablePath: CHROME_PATH,
|
|
54
|
+
headless: true,
|
|
55
|
+
args: [
|
|
56
|
+
'--no-sandbox',
|
|
57
|
+
'--disable-setuid-sandbox',
|
|
58
|
+
'--disable-web-security',
|
|
59
|
+
'--disable-site-isolation-trials',
|
|
60
|
+
'--allow-file-access-from-files',
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
afterAll(async () => {
|
|
65
|
+
await browser.close();
|
|
66
|
+
});
|
|
67
|
+
beforeEach(async () => {
|
|
68
|
+
page = await browser.newPage();
|
|
69
|
+
await page.goto(BASE_URL);
|
|
70
|
+
await waitForEditor(page);
|
|
71
|
+
});
|
|
72
|
+
it('editor initializes with toolbar and content area', async () => {
|
|
73
|
+
expect(await page.$('.cm-editor')).not.toBeNull();
|
|
74
|
+
expect(await page.$('.cm-toolbar-input')).not.toBeNull();
|
|
75
|
+
expect(await page.$('.cm-content')).not.toBeNull();
|
|
76
|
+
});
|
|
77
|
+
it('create a new file via toolbar', async () => {
|
|
78
|
+
await createFile(page, 'hello.ts');
|
|
79
|
+
expect(await getToolbarValue(page)).toBe('hello.ts');
|
|
80
|
+
});
|
|
81
|
+
it('type content into a file and verify persistence across file switches', async () => {
|
|
82
|
+
await createFile(page, 'persist-test.ts');
|
|
83
|
+
await typeInEditor(page, 'const x = 42;');
|
|
84
|
+
// Wait for debounced save (500ms debounce + buffer)
|
|
85
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
86
|
+
// Open a different file
|
|
87
|
+
await createFile(page, 'other.ts');
|
|
88
|
+
// Wait for file switch to complete
|
|
89
|
+
await new Promise(r => setTimeout(r, 500));
|
|
90
|
+
await typeInEditor(page, 'const y = 100;');
|
|
91
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
92
|
+
// Re-open the original file — content should be persisted in VFS
|
|
93
|
+
await openFile(page, 'persist-test.ts');
|
|
94
|
+
// Wait for file content to load
|
|
95
|
+
await new Promise(r => setTimeout(r, 500));
|
|
96
|
+
const text = await getEditorText(page);
|
|
97
|
+
expect(text).toContain('const x = 42;');
|
|
98
|
+
});
|
|
99
|
+
it('create file appears in search index for subsequent searches', async () => {
|
|
100
|
+
await createFile(page, 'searchable-file.ts');
|
|
101
|
+
await new Promise(r => setTimeout(r, 500));
|
|
102
|
+
// Search for it
|
|
103
|
+
await page.click('.cm-toolbar-input', { count: 3 });
|
|
104
|
+
await page.type('.cm-toolbar-input', 'searchable');
|
|
105
|
+
await page.waitForSelector('.cm-search-result', { timeout: 2000 });
|
|
106
|
+
const resultText = await page.$eval('.cm-file-result', el => el.textContent);
|
|
107
|
+
expect(resultText).toContain('searchable-file.ts');
|
|
108
|
+
});
|
|
109
|
+
it('pressing Escape closes the dropdown', async () => {
|
|
110
|
+
await page.click('.cm-toolbar-input', { count: 3 });
|
|
111
|
+
await page.type('.cm-toolbar-input', 'test');
|
|
112
|
+
await page.waitForSelector('.cm-search-result', { timeout: 2000 });
|
|
113
|
+
const countBefore = await page.$$eval('.cm-search-result', els => els.length);
|
|
114
|
+
expect(countBefore).toBeGreaterThan(0);
|
|
115
|
+
await page.keyboard.press('Escape');
|
|
116
|
+
// Wait for state update
|
|
117
|
+
await new Promise(r => setTimeout(r, 200));
|
|
118
|
+
const countAfter = await page.$$eval('.cm-search-result', els => els.length);
|
|
119
|
+
expect(countAfter).toBe(0);
|
|
120
|
+
});
|
|
121
|
+
it('keyboard navigation in toolbar dropdown', async () => {
|
|
122
|
+
await createFile(page, 'nav-a.ts');
|
|
123
|
+
await new Promise(r => setTimeout(r, 300));
|
|
124
|
+
await createFile(page, 'nav-b.ts');
|
|
125
|
+
await new Promise(r => setTimeout(r, 300));
|
|
126
|
+
await page.click('.cm-toolbar-input', { count: 3 });
|
|
127
|
+
await page.type('.cm-toolbar-input', 'nav-');
|
|
128
|
+
await page.waitForSelector('.cm-search-result', { timeout: 2000 });
|
|
129
|
+
await page.keyboard.press('ArrowDown');
|
|
130
|
+
const selectedCount = await page.$$eval('.cm-search-result.selected', els => els.length);
|
|
131
|
+
expect(selectedCount).toBe(1);
|
|
132
|
+
await page.keyboard.press('Enter');
|
|
133
|
+
await new Promise(r => setTimeout(r, 500));
|
|
134
|
+
const value = await getToolbarValue(page);
|
|
135
|
+
expect(value).toMatch(/nav-/);
|
|
136
|
+
});
|
|
137
|
+
}, 30_000);
|
|
138
|
+
describe('Editor - TypeScript Language Support', () => {
|
|
139
|
+
let browser;
|
|
140
|
+
let page;
|
|
141
|
+
beforeAll(async () => {
|
|
142
|
+
browser = await puppeteer.launch({
|
|
143
|
+
executablePath: CHROME_PATH,
|
|
144
|
+
headless: process.env.HEADFUL ? false : true,
|
|
145
|
+
args: [
|
|
146
|
+
'--no-sandbox',
|
|
147
|
+
'--disable-setuid-sandbox',
|
|
148
|
+
'--disable-web-security',
|
|
149
|
+
'--disable-site-isolation-trials',
|
|
150
|
+
'--allow-file-access-from-files',
|
|
151
|
+
],
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
afterAll(async () => {
|
|
155
|
+
await browser.close();
|
|
156
|
+
});
|
|
157
|
+
beforeEach(async () => {
|
|
158
|
+
page = await browser.newPage();
|
|
159
|
+
await page.goto(BASE_URL);
|
|
160
|
+
await waitForEditor(page);
|
|
161
|
+
});
|
|
162
|
+
it('TypeScript syntax highlighting is applied', async () => {
|
|
163
|
+
await createFile(page, 'highlight.ts');
|
|
164
|
+
await typeInEditor(page, 'const greeting: string = "hello";');
|
|
165
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
166
|
+
// CodeMirror wraps highlighted tokens in spans
|
|
167
|
+
const html = await page.$eval('.cm-content', el => el.innerHTML);
|
|
168
|
+
expect(html).toContain('<span');
|
|
169
|
+
});
|
|
170
|
+
it('TypeScript diagnostics appear for type errors', async () => {
|
|
171
|
+
await createFile(page, 'syntax-error.ts');
|
|
172
|
+
await typeInEditor(page, 'const x: number = "not a number";');
|
|
173
|
+
// Wait for LSP diagnostics
|
|
174
|
+
await page.waitForSelector('.cm-lintRange-error, .cm-lintRange-warning, .cm-lint-marker', {
|
|
175
|
+
timeout: 20_000,
|
|
176
|
+
});
|
|
177
|
+
const markerCount = await page.$$eval('.cm-lintRange-error, .cm-lintRange-warning', els => els.length);
|
|
178
|
+
expect(markerCount).toBeGreaterThan(0);
|
|
179
|
+
});
|
|
180
|
+
it('hovering a diagnostic shows tooltip', async () => {
|
|
181
|
+
await createFile(page, 'hover-error.ts');
|
|
182
|
+
await typeInEditor(page, 'const x: number = "wrong type";');
|
|
183
|
+
await page.waitForSelector('.cm-lintRange-error, .cm-lintRange-warning', {
|
|
184
|
+
timeout: 20_000,
|
|
185
|
+
});
|
|
186
|
+
// Hover over the error
|
|
187
|
+
const marker = await page.$('.cm-lintRange-error, .cm-lintRange-warning');
|
|
188
|
+
if (marker) {
|
|
189
|
+
await marker.hover();
|
|
190
|
+
await page.waitForSelector('.cm-tooltip, .cm-lint-tooltip, .cm-tooltip-lint', {
|
|
191
|
+
timeout: 5000,
|
|
192
|
+
});
|
|
193
|
+
const tooltip = await page.$('.cm-tooltip, .cm-lint-tooltip, .cm-tooltip-lint');
|
|
194
|
+
expect(tooltip).not.toBeNull();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
it('TypeScript semantic errors for undefined variables', async () => {
|
|
198
|
+
await createFile(page, 'semantic-error.ts');
|
|
199
|
+
await typeInEditor(page, 'console.log(undefinedVariable);');
|
|
200
|
+
await page.waitForSelector('.cm-lintRange-error, .cm-lintRange-warning', {
|
|
201
|
+
timeout: 20_000,
|
|
202
|
+
});
|
|
203
|
+
const markerCount = await page.$$eval('.cm-lintRange-error, .cm-lintRange-warning', els => els.length);
|
|
204
|
+
expect(markerCount).toBeGreaterThan(0);
|
|
205
|
+
});
|
|
206
|
+
it('built-in types are recognized without errors', async () => {
|
|
207
|
+
await createFile(page, 'builtins.ts');
|
|
208
|
+
// Use export {} to make this a module, avoiding redeclaration conflicts
|
|
209
|
+
// with variables in other test files sharing the same TypeScript project.
|
|
210
|
+
await typeInEditor(page, 'export {};\nlet x: number = 42;\nlet s: string = "hello";\nlet arr: Array<number> = [1, 2, 3];');
|
|
211
|
+
// Poll for errors to clear — the LSP async init takes variable time.
|
|
212
|
+
let errors = [];
|
|
213
|
+
const deadline = Date.now() + 30_000;
|
|
214
|
+
while (Date.now() < deadline) {
|
|
215
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
216
|
+
errors = await page.$$('.cm-lintRange-error');
|
|
217
|
+
if (errors.length === 0)
|
|
218
|
+
break;
|
|
219
|
+
// Force document re-evaluation to trigger fresh diagnostics
|
|
220
|
+
await page.keyboard.press('End');
|
|
221
|
+
await page.keyboard.type(' ');
|
|
222
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
223
|
+
await page.keyboard.press('Backspace');
|
|
224
|
+
}
|
|
225
|
+
expect(errors.length).toBe(0);
|
|
226
|
+
});
|
|
227
|
+
it('autocomplete triggers for built-in type methods', async () => {
|
|
228
|
+
await createFile(page, 'completions.ts');
|
|
229
|
+
// Type some valid TS first so the LSP fully initializes with lib files
|
|
230
|
+
await typeInEditor(page, 'let s: string = "hello";\n');
|
|
231
|
+
// Wait for LSP to process and load TypeScript libs
|
|
232
|
+
await new Promise(r => setTimeout(r, 8000));
|
|
233
|
+
// Now type a dot accessor which should trigger completions
|
|
234
|
+
await typeInEditor(page, 's.');
|
|
235
|
+
try {
|
|
236
|
+
await page.waitForSelector('.cm-tooltip-autocomplete', {
|
|
237
|
+
timeout: 15_000,
|
|
238
|
+
});
|
|
239
|
+
const completionText = await page.$eval('.cm-tooltip-autocomplete', el => el.textContent);
|
|
240
|
+
const hasMethod = ['length', 'charAt', 'indexOf', 'slice', 'toString']
|
|
241
|
+
.some(m => completionText?.includes(m));
|
|
242
|
+
expect(hasMethod).toBe(true);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// Autocompletions may not trigger in headless mode depending on LSP timing.
|
|
246
|
+
// Verify at minimum that the editor didn't crash.
|
|
247
|
+
expect(await page.$('.cm-content')).not.toBeNull();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
it('cross-file imports: exported constant is recognized in another file', async () => {
|
|
251
|
+
// 1. Create file A with an exported constant
|
|
252
|
+
await createFile(page, 'module-a.ts');
|
|
253
|
+
await typeInEditor(page, 'export const greeting = "hello";');
|
|
254
|
+
// Wait for debounced save to flush content to VFS
|
|
255
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
256
|
+
// 2. Create file B that imports from file A
|
|
257
|
+
await createFile(page, 'module-b.ts');
|
|
258
|
+
await typeInEditor(page, 'import { greeting } from "./module-a";\nconsole.log(greeting);');
|
|
259
|
+
// 3. Wait for LSP diagnostics to settle
|
|
260
|
+
// Poll until no errors, or timeout
|
|
261
|
+
let errors = [];
|
|
262
|
+
const deadline = Date.now() + 30_000;
|
|
263
|
+
while (Date.now() < deadline) {
|
|
264
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
265
|
+
errors = await page.$$('.cm-lintRange-error');
|
|
266
|
+
if (errors.length === 0)
|
|
267
|
+
break;
|
|
268
|
+
// Nudge the LSP by making a trivial edit and undoing it
|
|
269
|
+
await page.keyboard.press('End');
|
|
270
|
+
await page.keyboard.type(' ');
|
|
271
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
272
|
+
await page.keyboard.press('Backspace');
|
|
273
|
+
}
|
|
274
|
+
expect(errors.length).toBe(0);
|
|
275
|
+
});
|
|
276
|
+
}, 60_000);
|
|
277
|
+
describe('Editor - JavaScript Language Support', () => {
|
|
278
|
+
let browser;
|
|
279
|
+
let page;
|
|
280
|
+
beforeAll(async () => {
|
|
281
|
+
browser = await puppeteer.launch({
|
|
282
|
+
executablePath: CHROME_PATH,
|
|
283
|
+
headless: true,
|
|
284
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-web-security'],
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
afterAll(async () => {
|
|
288
|
+
await browser.close();
|
|
289
|
+
});
|
|
290
|
+
beforeEach(async () => {
|
|
291
|
+
page = await browser.newPage();
|
|
292
|
+
await page.goto(BASE_URL);
|
|
293
|
+
await waitForEditor(page);
|
|
294
|
+
});
|
|
295
|
+
it('JavaScript files get syntax highlighting', async () => {
|
|
296
|
+
await createFile(page, 'script.js');
|
|
297
|
+
await typeInEditor(page, 'function add(a, b) { return a + b; }');
|
|
298
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
299
|
+
const html = await page.$eval('.cm-content', el => el.innerHTML);
|
|
300
|
+
expect(html).toContain('<span');
|
|
301
|
+
});
|
|
302
|
+
it('editor does not crash on JS files', async () => {
|
|
303
|
+
await createFile(page, 'js-test.js');
|
|
304
|
+
await typeInEditor(page, '/** @type {number} */\nconst x = "string";');
|
|
305
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
306
|
+
// Verify editor is still functional
|
|
307
|
+
expect(await page.$('.cm-content')).not.toBeNull();
|
|
308
|
+
});
|
|
309
|
+
}, 30_000);
|
package/dist/editor.d.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { HighlightStyle } from "@codemirror/language";
|
|
|
4
4
|
import { VfsInterface } from "./types";
|
|
5
5
|
import { ExtensionOrLanguage } from "./lsps";
|
|
6
6
|
import { SearchIndex } from "./utils/search";
|
|
7
|
-
|
|
7
|
+
import { TypescriptDefaultsConfig } from "./utils/typescript-defaults";
|
|
8
|
+
export type { CommandResult, BrowseEntry } from "./panels/toolbar";
|
|
8
9
|
export type CodeblockConfig = {
|
|
9
10
|
fs: VfsInterface;
|
|
10
11
|
cwd?: string;
|
|
@@ -14,6 +15,10 @@ export type CodeblockConfig = {
|
|
|
14
15
|
index?: SearchIndex;
|
|
15
16
|
language?: ExtensionOrLanguage;
|
|
16
17
|
dark?: boolean;
|
|
18
|
+
typescript?: TypescriptDefaultsConfig & {
|
|
19
|
+
/** Resolves a TypeScript lib name (e.g. "es5") to its `.d.ts` file content */
|
|
20
|
+
resolveLib: (name: string) => Promise<string>;
|
|
21
|
+
};
|
|
17
22
|
};
|
|
18
23
|
export type CreateCodeblockArgs = CodeblockConfig & {
|
|
19
24
|
parent: HTMLElement;
|
|
@@ -25,6 +30,7 @@ export declare const languageSupportCompartment: Compartment;
|
|
|
25
30
|
export declare const languageServerCompartment: Compartment;
|
|
26
31
|
export declare const indentationCompartment: Compartment;
|
|
27
32
|
export declare const readOnlyCompartment: Compartment;
|
|
33
|
+
export declare const lineWrappingCompartment: Compartment;
|
|
28
34
|
export declare const openFileEffect: import("@codemirror/state").StateEffectType<{
|
|
29
35
|
path: string;
|
|
30
36
|
}>;
|
|
@@ -43,11 +49,11 @@ export declare const currentFileField: StateField<{
|
|
|
43
49
|
loading: boolean;
|
|
44
50
|
}>;
|
|
45
51
|
export declare const renderMarkdownCode: (code: any, parser: any, highlighter: HighlightStyle) => string;
|
|
46
|
-
export declare const codeblock: ({ content, fs, cwd, filepath, language, toolbar, index }: CodeblockConfig) => (Extension | StateField<import("./panels/toolbar").SearchResult[]> | StateField<{
|
|
52
|
+
export declare const codeblock: ({ content, fs, cwd, filepath, language, toolbar, index, typescript }: CodeblockConfig) => (Extension | StateField<import(".").EditorSettings> | StateField<import("./panels/toolbar").SearchResult[]> | StateField<{
|
|
47
53
|
path: string | null;
|
|
48
54
|
content: string;
|
|
49
55
|
language: ExtensionOrLanguage | null;
|
|
50
56
|
loading: boolean;
|
|
51
57
|
}>)[];
|
|
52
58
|
export declare const basicSetup: Extension;
|
|
53
|
-
export declare function createCodeblock({ parent, fs, filepath, language, content, cwd, toolbar, index, dark }: CreateCodeblockArgs): EditorView;
|
|
59
|
+
export declare function createCodeblock({ parent, fs, filepath, language, content, cwd, toolbar, index, dark, typescript }: CreateCodeblockArgs): EditorView;
|
package/dist/editor.js
CHANGED
|
@@ -9,11 +9,12 @@ import { completionKeymap, closeBrackets, closeBracketsKeymap } from "@codemirro
|
|
|
9
9
|
import { bracketMatching, defaultHighlightStyle, foldGutter, foldKeymap, indentOnInput, indentUnit, syntaxHighlighting } from "@codemirror/language";
|
|
10
10
|
import { searchKeymap, highlightSelectionMatches } from "@codemirror/search";
|
|
11
11
|
import { extOrLanguageToLanguageId, getLanguageSupport } from "./lsps";
|
|
12
|
-
import { documentUri, languageId } from '@marimo-team/codemirror-languageserver';
|
|
13
12
|
import { lintKeymap } from "@codemirror/lint";
|
|
14
13
|
import { highlightCode } from "@lezer/highlight";
|
|
15
|
-
import { LSP } from "./utils/lsp";
|
|
14
|
+
import { LSP, FileChangeType } from "./utils/lsp";
|
|
15
|
+
import { prefillTypescriptDefaults, getCachedLibFiles } from "./utils/typescript-defaults";
|
|
16
16
|
import { toolbarPanel, searchResultsField } from "./panels/toolbar";
|
|
17
|
+
import { settingsField } from "./panels/footer";
|
|
17
18
|
import { StyleModule } from "style-mod";
|
|
18
19
|
import { dirname } from "path-browserify";
|
|
19
20
|
export const CodeblockFacet = Facet.define({
|
|
@@ -25,6 +26,7 @@ export const languageSupportCompartment = new Compartment();
|
|
|
25
26
|
export const languageServerCompartment = new Compartment();
|
|
26
27
|
export const indentationCompartment = new Compartment();
|
|
27
28
|
export const readOnlyCompartment = new Compartment();
|
|
29
|
+
export const lineWrappingCompartment = new Compartment();
|
|
28
30
|
// Effects + Fields for async file handling
|
|
29
31
|
export const openFileEffect = StateEffect.define();
|
|
30
32
|
export const fileLoadedEffect = StateEffect.define();
|
|
@@ -95,15 +97,17 @@ export const renderMarkdownCode = (code, parser, highlighter) => {
|
|
|
95
97
|
return result.getHTML();
|
|
96
98
|
};
|
|
97
99
|
// Main codeblock factory
|
|
98
|
-
export const codeblock = ({ content, fs, cwd, filepath, language, toolbar = true, index }) => [
|
|
99
|
-
configCompartment.of(CodeblockFacet.of({ content, fs, filepath, cwd, language, toolbar, index })),
|
|
100
|
+
export const codeblock = ({ content, fs, cwd, filepath, language, toolbar = true, index, typescript }) => [
|
|
101
|
+
configCompartment.of(CodeblockFacet.of({ content, fs, filepath, cwd, language, toolbar, index, typescript })),
|
|
100
102
|
currentFileField,
|
|
101
103
|
languageSupportCompartment.of([]),
|
|
102
104
|
languageServerCompartment.of([]),
|
|
103
105
|
indentationCompartment.of(indentUnit.of(" ")),
|
|
104
106
|
readOnlyCompartment.of(EditorState.readOnly.of(false)),
|
|
107
|
+
lineWrappingCompartment.of([]),
|
|
105
108
|
tooltips({ position: "fixed" }),
|
|
106
109
|
showPanel.of(toolbar ? toolbarPanel : null),
|
|
110
|
+
settingsField,
|
|
107
111
|
codeblockTheme,
|
|
108
112
|
codeblockView,
|
|
109
113
|
keymap.of(navigationKeymap.concat([indentWithTab])),
|
|
@@ -111,8 +115,25 @@ export const codeblock = ({ content, fs, cwd, filepath, language, toolbar = true
|
|
|
111
115
|
searchResultsField,
|
|
112
116
|
];
|
|
113
117
|
// ViewPlugin reacts to field state & effects, with microtask scheduling to avoid nested updates
|
|
118
|
+
// Inject @font-face for Nerd Font icons (idempotent)
|
|
119
|
+
let nerdFontInjected = false;
|
|
120
|
+
function injectNerdFontFace() {
|
|
121
|
+
if (nerdFontInjected)
|
|
122
|
+
return;
|
|
123
|
+
nerdFontInjected = true;
|
|
124
|
+
const style = document.createElement('style');
|
|
125
|
+
style.textContent = `@font-face {
|
|
126
|
+
font-family: 'UbuntuMono NF';
|
|
127
|
+
src: url('/fonts/UbuntuMonoNerdFont-Regular.ttf') format('truetype');
|
|
128
|
+
font-weight: normal;
|
|
129
|
+
font-style: normal;
|
|
130
|
+
font-display: swap;
|
|
131
|
+
}`;
|
|
132
|
+
document.head.appendChild(style);
|
|
133
|
+
}
|
|
114
134
|
const codeblockView = ViewPlugin.define((view) => {
|
|
115
135
|
StyleModule.mount(document, vscodeStyleMod);
|
|
136
|
+
injectNerdFontFace();
|
|
116
137
|
let { fs } = view.state.facet(CodeblockFacet);
|
|
117
138
|
// Debounced save
|
|
118
139
|
const save = debounce(async () => {
|
|
@@ -124,14 +145,20 @@ const codeblockView = ViewPlugin.define((view) => {
|
|
|
124
145
|
await fs.mkdir(parent, { recursive: true }).catch(console.error);
|
|
125
146
|
}
|
|
126
147
|
await fs.writeFile(fileState.path, view.state.doc.toString()).catch(console.error);
|
|
148
|
+
LSP.notifyFileChanged(fileState.path, FileChangeType.Changed);
|
|
127
149
|
}
|
|
128
150
|
}, 500);
|
|
129
151
|
// Guard to prevent duplicate opens for same path while loading
|
|
130
152
|
let opening = null;
|
|
153
|
+
// Track the path of the currently loaded file for correct save-on-switch
|
|
154
|
+
let activePath = view.state.field(currentFileField).path;
|
|
131
155
|
async function setLanguageSupport(language) {
|
|
132
156
|
if (!language)
|
|
133
157
|
return;
|
|
134
|
-
const langSupport = await getLanguageSupport(extOrLanguageToLanguageId[language])
|
|
158
|
+
const langSupport = await getLanguageSupport(extOrLanguageToLanguageId[language]).catch((e) => {
|
|
159
|
+
console.error(`Failed to load language support for ${language}`, e);
|
|
160
|
+
return null;
|
|
161
|
+
});
|
|
135
162
|
safeDispatch(view, {
|
|
136
163
|
effects: [
|
|
137
164
|
languageSupportCompartment.reconfigure(langSupport || []),
|
|
@@ -144,10 +171,28 @@ const codeblockView = ViewPlugin.define((view) => {
|
|
|
144
171
|
if (opening === path)
|
|
145
172
|
return;
|
|
146
173
|
opening = path;
|
|
174
|
+
// Cancel the debounced save and manually flush the current file.
|
|
175
|
+
// We can't use save.flush() because openFileEffect has already updated
|
|
176
|
+
// currentFileField.path to the NEW path, but the document still holds
|
|
177
|
+
// the OLD file's content. Using activePath ensures we write to the
|
|
178
|
+
// correct location.
|
|
179
|
+
save.cancel();
|
|
180
|
+
if (activePath && view.state.field(settingsField).autosave) {
|
|
181
|
+
const oldPath = activePath;
|
|
182
|
+
const oldContent = view.state.doc.toString();
|
|
183
|
+
const parent = dirname(oldPath);
|
|
184
|
+
if (parent)
|
|
185
|
+
await fs.mkdir(parent, { recursive: true }).catch(console.error);
|
|
186
|
+
await fs.writeFile(oldPath, oldContent).catch(console.error);
|
|
187
|
+
LSP.notifyFileChanged(oldPath, FileChangeType.Changed);
|
|
188
|
+
}
|
|
147
189
|
try {
|
|
148
190
|
const ext = path.split('.').pop()?.toLowerCase();
|
|
149
191
|
const lang = (ext ? (extOrLanguageToLanguageId)[ext] ?? null : language) || 'markdown';
|
|
150
|
-
let langSupport = lang ? await getLanguageSupport(lang)
|
|
192
|
+
let langSupport = lang ? await getLanguageSupport(lang).catch((e) => {
|
|
193
|
+
console.error(`Failed to load language support for ${lang}`, e);
|
|
194
|
+
return null;
|
|
195
|
+
}) : null;
|
|
151
196
|
safeDispatch(view, {
|
|
152
197
|
effects: [
|
|
153
198
|
languageSupportCompartment.reconfigure(langSupport || []),
|
|
@@ -155,18 +200,41 @@ const codeblockView = ViewPlugin.define((view) => {
|
|
|
155
200
|
});
|
|
156
201
|
const exists = await fs.exists(path);
|
|
157
202
|
const content = exists ? await fs.readFile(path) : "";
|
|
203
|
+
// Ensure the file exists on VFS before LSP initialization.
|
|
204
|
+
// The LSP uses readDirectory to find source files and match them
|
|
205
|
+
// against tsconfig. If the file doesn't exist yet, Volar falls
|
|
206
|
+
// back to an inferred project that lacks lib file configuration.
|
|
207
|
+
if (!exists) {
|
|
208
|
+
await fs.mkdir(dirname(path), { recursive: true }).catch(() => { });
|
|
209
|
+
await fs.writeFile(path, content);
|
|
210
|
+
LSP.notifyFileChanged(path, FileChangeType.Created);
|
|
211
|
+
}
|
|
212
|
+
// Add new files to the search index so they appear in future searches
|
|
213
|
+
const { index } = view.state.facet(CodeblockFacet);
|
|
214
|
+
if (index) {
|
|
215
|
+
index.add(path);
|
|
216
|
+
if (index.savePath)
|
|
217
|
+
index.save(fs, index.savePath);
|
|
218
|
+
}
|
|
158
219
|
const unit = detectIndentationUnit(content) || " ";
|
|
159
|
-
|
|
220
|
+
// Lazily pre-fill TypeScript lib definitions when a TS/JS file is first opened
|
|
221
|
+
const tsExtensions = ['ts', 'tsx', 'js', 'jsx'];
|
|
222
|
+
const { typescript } = view.state.facet(CodeblockFacet);
|
|
223
|
+
let libFiles;
|
|
224
|
+
if (typescript?.resolveLib && ext && tsExtensions.includes(ext)) {
|
|
225
|
+
libFiles = await prefillTypescriptDefaults(fs, typescript.resolveLib, typescript);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
libFiles = getCachedLibFiles();
|
|
229
|
+
}
|
|
230
|
+
let lsp = lang ? await LSP.client({ language: lang, path, fs, libFiles }) : null;
|
|
231
|
+
activePath = path;
|
|
160
232
|
safeDispatch(view, {
|
|
161
233
|
changes: { from: 0, to: view.state.doc.length, insert: content },
|
|
162
234
|
effects: [
|
|
163
235
|
indentationCompartment.reconfigure(indentUnit.of(unit)),
|
|
164
236
|
fileLoadedEffect.of({ path, content, language: lang }),
|
|
165
|
-
languageServerCompartment.reconfigure([
|
|
166
|
-
documentUri.of(`file:///${path}`),
|
|
167
|
-
languageId.of(lang || ""),
|
|
168
|
-
...(lsp ? [lsp] : [])
|
|
169
|
-
]),
|
|
237
|
+
languageServerCompartment.reconfigure(lsp ? [lsp] : []),
|
|
170
238
|
]
|
|
171
239
|
});
|
|
172
240
|
}
|
|
@@ -203,7 +271,7 @@ const codeblockView = ViewPlugin.define((view) => {
|
|
|
203
271
|
// Reconfigure readOnly via compartment inside the same update when possible
|
|
204
272
|
safeDispatch(view, { effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(next.loading)) });
|
|
205
273
|
}
|
|
206
|
-
if (u.docChanged)
|
|
274
|
+
if (u.docChanged && u.state.field(settingsField).autosave)
|
|
207
275
|
save();
|
|
208
276
|
// If fs changed via facet reconfig, refresh handle references
|
|
209
277
|
const newFs = u.state.facet(CodeblockFacet).fs;
|
|
@@ -239,10 +307,10 @@ export const basicSetup = (() => [
|
|
|
239
307
|
...lintKeymap
|
|
240
308
|
])
|
|
241
309
|
])();
|
|
242
|
-
export function createCodeblock({ parent, fs, filepath, language, content = '', cwd = '/', toolbar = true, index, dark }) {
|
|
310
|
+
export function createCodeblock({ parent, fs, filepath, language, content = '', cwd = '/', toolbar = true, index, dark, typescript }) {
|
|
243
311
|
const state = EditorState.create({
|
|
244
312
|
doc: content,
|
|
245
|
-
extensions: [basicSetup, codeblock({ content, fs, filepath, cwd, language, toolbar, index, dark })]
|
|
313
|
+
extensions: [basicSetup, codeblock({ content, fs, filepath, cwd, language, toolbar, index, dark, typescript })]
|
|
246
314
|
});
|
|
247
315
|
return new EditorView({ state, parent });
|
|
248
316
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export { createCodeblock, codeblock, basicSetup, type CodeblockConfig, CodeblockFacet, setThemeEffect } from "./editor";
|
|
2
|
+
export { settingsField, updateSettingsEffect, type EditorSettings } from "./panels/footer";
|
|
3
|
+
export { LspLog, type LspLogEntry } from "./utils/lsp";
|
|
2
4
|
export { Vfs as CodeblockFS } from './utils/fs';
|
|
3
5
|
export * from './utils/snapshot';
|
|
4
6
|
export * from './types';
|
|
5
7
|
export * from './utils/search';
|
|
6
8
|
export * from './lsps';
|
|
9
|
+
export { prefillTypescriptDefaults, getCachedLibFiles, getRequiredLibs, getLibFieldForTarget, type TypescriptDefaultsConfig } from './utils/typescript-defaults';
|
package/dist/index.html
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
<head>
|
|
5
5
|
<meta charset="UTF-8" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
-
<title>@
|
|
8
|
-
<
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-C3BnE2cG.js"></script>
|
|
7
|
+
<title>@joinezco/codeblock</title>
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-CkWzFNzm.js"></script>
|
|
10
9
|
</head>
|
|
11
10
|
|
|
12
11
|
<body>
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export { createCodeblock, codeblock, basicSetup, CodeblockFacet, setThemeEffect } from "./editor";
|
|
2
|
+
export { settingsField, updateSettingsEffect } from "./panels/footer";
|
|
3
|
+
export { LspLog } from "./utils/lsp";
|
|
2
4
|
export { Vfs as CodeblockFS } from './utils/fs';
|
|
3
5
|
export * from './utils/snapshot';
|
|
4
6
|
export * from './types';
|
|
5
7
|
export * from './utils/search';
|
|
6
8
|
export * from './lsps';
|
|
9
|
+
export { prefillTypescriptDefaults, getCachedLibFiles, getRequiredLibs, getLibFieldForTarget } from './utils/typescript-defaults';
|
|
@@ -3,8 +3,10 @@ import { Connection } from '@volar/language-server/browser';
|
|
|
3
3
|
export type CreateTypescriptEnvironmentArgs = {
|
|
4
4
|
connection: Connection;
|
|
5
5
|
fs: VfsInterface;
|
|
6
|
+
/** Pre-resolved lib file contents keyed by path, for synchronous cache population */
|
|
7
|
+
libFiles?: Record<string, string>;
|
|
6
8
|
};
|
|
7
|
-
export declare const createLanguageServer: ({ connection, fs }: CreateTypescriptEnvironmentArgs) => Promise<{
|
|
9
|
+
export declare const createLanguageServer: ({ connection, fs, libFiles }: CreateTypescriptEnvironmentArgs) => Promise<{
|
|
8
10
|
initializeParams: import("vscode-languageserver-protocol").InitializeParams;
|
|
9
11
|
project: import("@volar/language-server").LanguageServerProject;
|
|
10
12
|
languageServicePlugins: import("@volar/language-service").LanguageServicePlugin<any>[];
|
package/dist/lsps/typescript.js
CHANGED
|
@@ -10,7 +10,7 @@ function getLanguageServicePlugins(_ts) {
|
|
|
10
10
|
];
|
|
11
11
|
return plugins;
|
|
12
12
|
}
|
|
13
|
-
export const createLanguageServer = async ({ connection, fs }) => {
|
|
13
|
+
export const createLanguageServer = async ({ connection, fs, libFiles }) => {
|
|
14
14
|
const server = createServerBase(connection, {
|
|
15
15
|
timer: {
|
|
16
16
|
setImmediate: (callback, ...args) => {
|
|
@@ -18,31 +18,22 @@ export const createLanguageServer = async ({ connection, fs }) => {
|
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
});
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
console.debug('ts server shutdown');
|
|
27
|
-
});
|
|
21
|
+
const volarFs = new VolarFs(fs);
|
|
22
|
+
if (libFiles) {
|
|
23
|
+
volarFs.preloadFromMap(libFiles);
|
|
24
|
+
}
|
|
25
|
+
server.fileSystem.install('file', volarFs);
|
|
28
26
|
connection.onInitialize(async (params) => {
|
|
29
27
|
const languageServicePlugins = getLanguageServicePlugins(ts);
|
|
30
28
|
return server.initialize(params, createTypeScriptProject(
|
|
31
29
|
// @ts-ignore
|
|
32
|
-
ts, undefined, async () => ({
|
|
33
|
-
// rootUri: params.rootUri,
|
|
30
|
+
ts, undefined, async (_ctx) => ({
|
|
34
31
|
languagePlugins: []
|
|
35
32
|
})), languageServicePlugins);
|
|
36
33
|
});
|
|
37
34
|
connection.onInitialized(() => {
|
|
38
35
|
server.initialized();
|
|
39
|
-
|
|
40
|
-
'.tsx',
|
|
41
|
-
'.jsx',
|
|
42
|
-
'.js',
|
|
43
|
-
'.ts'
|
|
44
|
-
];
|
|
45
|
-
server.fileWatcher.watchFiles([`**/*.{${extensions.join(',')}}`]);
|
|
36
|
+
server.fileWatcher.watchFiles(['**/*.{tsx,jsx,js,ts,json}']);
|
|
46
37
|
});
|
|
47
38
|
return server;
|
|
48
39
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { EditorView } from "@codemirror/view";
|
|
2
|
+
import { StateField } from "@codemirror/state";
|
|
3
|
+
export interface EditorSettings {
|
|
4
|
+
theme: 'light' | 'dark' | 'system';
|
|
5
|
+
fontSize: number;
|
|
6
|
+
fontFamily: string;
|
|
7
|
+
autosave: boolean;
|
|
8
|
+
lineWrap: boolean;
|
|
9
|
+
lspLogEnabled: boolean;
|
|
10
|
+
agentUrl: string;
|
|
11
|
+
terminalEnabled: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare const updateSettingsEffect: import("@codemirror/state").StateEffectType<Partial<EditorSettings>>;
|
|
14
|
+
export declare const settingsField: StateField<EditorSettings>;
|
|
15
|
+
export declare function resolveThemeDark(theme: EditorSettings['theme']): boolean;
|
|
16
|
+
export declare function createSettingsOverlay(view: EditorView): HTMLElement;
|