@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.
Files changed (54) hide show
  1. package/dist/assets/fs.worker-DfanUHpQ.js +21 -0
  2. package/dist/assets/{index-MGle_v2x.js → index-BAnLzvMk.js} +1 -1
  3. package/dist/assets/{index-as7ELo0J.js → index-BBC9WDX6.js} +1 -1
  4. package/dist/assets/{index-Dx_VuNNd.js → index-BEXYxRro.js} +1 -1
  5. package/dist/assets/{index-pGm0qkrJ.js → index-BfYmUKH9.js} +1 -1
  6. package/dist/assets/{index-CXFONXS8.js → index-BhaTNAWE.js} +1 -1
  7. package/dist/assets/{index-D5Z27j1C.js → index-CCbYDSng.js} +1 -1
  8. package/dist/assets/{index-Dvu-FFzd.js → index-CIi8tLT6.js} +1 -1
  9. package/dist/assets/{index-C-QhPFHP.js → index-CaANcgI2.js} +1 -1
  10. package/dist/assets/index-CkWzFNzm.js +208 -0
  11. package/dist/assets/{index-N-GE7HTU.js → index-D_XGv9QZ.js} +1 -1
  12. package/dist/assets/{index-DWOBdRjn.js → index-DkmiPfkD.js} +1 -1
  13. package/dist/assets/{index-CGx5MZO7.js → index-DmNlLMQ4.js} +1 -1
  14. package/dist/assets/{index-I0dlv-r3.js → index-DmX_vI7D.js} +1 -1
  15. package/dist/assets/{index-9HdhmM_Y.js → index-DogEEevD.js} +1 -1
  16. package/dist/assets/{index-aEsF5o-7.js → index-DsDl5qZV.js} +1 -1
  17. package/dist/assets/{index-gUUzXNuP.js → index-gAy5mDg-.js} +1 -1
  18. package/dist/assets/{index-CIuq3uTk.js → index-i5qJLB2h.js} +1 -1
  19. package/dist/assets/javascript.worker-ClsyHOLi.js +552 -0
  20. package/dist/e2e/editor.spec.d.ts +1 -0
  21. package/dist/e2e/editor.spec.js +309 -0
  22. package/dist/editor.d.ts +9 -3
  23. package/dist/editor.js +83 -15
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.html +2 -3
  26. package/dist/index.js +3 -0
  27. package/dist/lsps/typescript.d.ts +3 -1
  28. package/dist/lsps/typescript.js +8 -17
  29. package/dist/panels/footer.d.ts +16 -0
  30. package/dist/panels/footer.js +258 -0
  31. package/dist/panels/toolbar.d.ts +15 -2
  32. package/dist/panels/toolbar.js +528 -115
  33. package/dist/panels/toolbar.test.js +20 -14
  34. package/dist/rpc/transport.d.ts +2 -11
  35. package/dist/rpc/transport.js +19 -35
  36. package/dist/themes/index.js +181 -14
  37. package/dist/themes/vscode.js +3 -2
  38. package/dist/utils/fs.d.ts +15 -3
  39. package/dist/utils/fs.js +85 -6
  40. package/dist/utils/lsp.d.ts +26 -15
  41. package/dist/utils/lsp.js +79 -44
  42. package/dist/utils/search.d.ts +2 -0
  43. package/dist/utils/search.js +13 -4
  44. package/dist/utils/typescript-defaults.d.ts +57 -0
  45. package/dist/utils/typescript-defaults.js +208 -0
  46. package/dist/utils/typescript-defaults.test.d.ts +1 -0
  47. package/dist/utils/typescript-defaults.test.js +197 -0
  48. package/dist/workers/fs.worker.js +14 -18
  49. package/dist/workers/javascript.worker.js +11 -9
  50. package/package.json +4 -3
  51. package/dist/assets/fs.worker-BwEqZcql.ts +0 -109
  52. package/dist/assets/index-C3BnE2cG.js +0 -222
  53. package/dist/assets/javascript.worker-C1zGArKk.js +0 -527
  54. package/dist/snapshot.bin +0 -0
@@ -1,3 +1,4 @@
1
+ // @vitest-environment jsdom
1
2
  import { describe, it, expect, beforeEach, vi } from 'vitest';
2
3
  import { EditorState } from '@codemirror/state';
3
4
  import { EditorView } from '@codemirror/view';
@@ -13,19 +14,25 @@ vi.mock('../lsps', () => ({
13
14
  'go': 'go'
14
15
  }
15
16
  }));
16
- vi.mock('../editor', () => ({
17
- CodeblockFacet: {
18
- of: vi.fn(),
19
- },
20
- currentFileField: {
21
- init: vi.fn(() => ({ path: null, content: '', language: null, loading: false }))
22
- },
23
- openFileEffect: {
24
- of: vi.fn()
25
- }
26
- }));
17
+ vi.mock('../editor', async () => {
18
+ const { Facet, StateField, StateEffect } = await import('@codemirror/state');
19
+ return {
20
+ CodeblockFacet: Facet.define({
21
+ combine: (values) => values[0],
22
+ }),
23
+ currentFileField: StateField.define({
24
+ create() {
25
+ return { path: null, content: '', language: null, loading: false };
26
+ },
27
+ update(value) {
28
+ return value;
29
+ },
30
+ }),
31
+ openFileEffect: StateEffect.define(),
32
+ };
33
+ });
27
34
  describe('Toolbar Panel', () => {
28
- let view;
35
+ const ctx = { view: null };
29
36
  let mockFs;
30
37
  beforeEach(() => {
31
38
  mockFs = {
@@ -49,11 +56,10 @@ describe('Toolbar Panel', () => {
49
56
  currentFileField
50
57
  ]
51
58
  });
52
- view = new EditorView({
59
+ ctx.view = new EditorView({
53
60
  state,
54
61
  parent: document.createElement('div')
55
62
  });
56
- console.log(view);
57
63
  });
58
64
  describe('Command Results Generation', () => {
59
65
  it('should generate create file command for any query', () => {
@@ -1,11 +1,2 @@
1
- import { Transport } from "@open-rpc/client-js/build/transports/Transport";
2
- import type { JSONRPCRequestData } from "@open-rpc/client-js/src/Request";
3
- export default class MessagePortTransport extends Transport {
4
- port: MessagePort;
5
- postMessageID: string;
6
- constructor(port: MessagePort);
7
- private messageHandler;
8
- connect(): Promise<void>;
9
- sendData(data: JSONRPCRequestData): Promise<any>;
10
- close(): void;
11
- }
1
+ import type { Transport } from "@codemirror/lsp-client";
2
+ export declare function messagePortTransport(port: MessagePort): Transport;
@@ -1,38 +1,22 @@
1
- import { Transport } from "@open-rpc/client-js/build/transports/Transport";
2
- import { getNotifications } from "@open-rpc/client-js/src/Request";
3
- export default class MessagePortTransport extends Transport {
4
- port;
5
- postMessageID;
6
- constructor(port) {
7
- super();
8
- this.port = port;
9
- this.postMessageID = `post-message-transport-${Math.random()}`;
10
- }
11
- messageHandler = (ev) => {
12
- console.debug("LSP <<-", ev.data);
13
- if (!ev.data.result && ev.data.method !== 'textDocument/publishDiagnostics') {
14
- console.debug(ev.data);
15
- // this.port.postMessage({ jsonrpc: '2.0', id: ev.data.id, result: null })
16
- }
17
- else {
18
- this.transportRequestManager.resolveResponse(JSON.stringify(ev.data));
1
+ /// Creates a Transport adapter that bridges a MessagePort (which
2
+ /// sends/receives JSON objects) to lsp-client's Transport interface
3
+ /// (which sends/receives JSON strings).
4
+ export function messagePortTransport(port) {
5
+ let handlers = [];
6
+ port.addEventListener("message", (ev) => {
7
+ let msg = typeof ev.data === "string" ? ev.data : JSON.stringify(ev.data);
8
+ for (let handler of handlers)
9
+ handler(msg);
10
+ });
11
+ return {
12
+ send(message) {
13
+ port.postMessage(JSON.parse(message));
14
+ },
15
+ subscribe(handler) {
16
+ handlers.push(handler);
17
+ },
18
+ unsubscribe(handler) {
19
+ handlers = handlers.filter(h => h !== handler);
19
20
  }
20
21
  };
21
- connect() {
22
- return new Promise(async (resolve) => {
23
- this.port.addEventListener("message", this.messageHandler);
24
- resolve();
25
- });
26
- }
27
- async sendData(data) {
28
- console.debug("LSP ->>", data);
29
- const prom = this.transportRequestManager.addRequest(data, null);
30
- const notifications = getNotifications(data);
31
- if (this.port) {
32
- this.port.postMessage(data.request);
33
- this.transportRequestManager.settlePendingRequest(notifications);
34
- }
35
- return prom;
36
- }
37
- close() { }
38
22
  }
@@ -1,4 +1,10 @@
1
1
  import { EditorView } from '@codemirror/view';
2
+ // Font size helpers — all relative to --cm-font-size so changing the
3
+ // base font size in settings automatically scales the entire UI.
4
+ const FS = 'var(--cm-font-size, 16px)';
5
+ const FS_75 = `calc(${FS} * 0.75)`; // 12px at base 16
6
+ const FS_85 = `calc(${FS} * 0.85)`; // ~14px at base 16
7
+ const FS_875 = `calc(${FS} * 0.875)`; // 14px at base 16
2
8
  export const codeblockTheme = EditorView.theme({
3
9
  "&:not(.cm-focused)": {
4
10
  '& .cm-activeLine, & .cm-activeLineGutter': {
@@ -12,7 +18,7 @@ export const codeblockTheme = EditorView.theme({
12
18
  border: 'none',
13
19
  background: 'transparent',
14
20
  outline: 'none',
15
- fontSize: '16px',
21
+ fontSize: FS,
16
22
  color: 'var(--cm-toolbar-color)',
17
23
  padding: '0 2px 0 6px',
18
24
  width: '100%',
@@ -43,10 +49,9 @@ export const codeblockTheme = EditorView.theme({
43
49
  '& > .cm-search-result-icon-container': {
44
50
  width: 'var(--cm-gutter-width)',
45
51
  '& > .cm-search-result-icon': {
46
- fontSize: '16px',
47
- textAlign: 'right',
52
+ fontSize: FS,
53
+ textAlign: 'center',
48
54
  boxSizing: 'border-box',
49
- padding: '0 3px 0 5px',
50
55
  width: 'var(--cm-gutter-lineno-width)',
51
56
  }
52
57
  },
@@ -69,14 +74,23 @@ export const codeblockTheme = EditorView.theme({
69
74
  },
70
75
  '.cm-toolbar-state-icon-container': {
71
76
  width: 'var(--cm-gutter-width)',
77
+ display: 'flex',
72
78
  },
79
+ // Nerd Font icon glyphs have visual widths (700-920 units) that far exceed
80
+ // their monospace advance width (500 units), overflowing to the right.
81
+ // text-align operates on the advance width, not the visual bounds, so
82
+ // 'right' misaligns the icon. 'center' partially compensates for the
83
+ // rightward overflow and visually aligns with gutter line numbers.
73
84
  '.cm-toolbar-state-icon': {
74
- fontSize: '16px',
75
- textAlign: 'right',
76
- boxSizing: 'border-box',
77
- padding: '0 3px 0 5px',
85
+ fontSize: FS,
78
86
  color: 'var(--cm-foreground)',
79
- width: 'var(--cm-gutter-lineno-width)'
87
+ fontFamily: 'var(--cm-icon-font-family)',
88
+ textAlign: 'center',
89
+ boxSizing: 'border-box',
90
+ width: 'var(--cm-gutter-lineno-width)',
91
+ },
92
+ '&': {
93
+ fontSize: FS,
80
94
  },
81
95
  '.cm-content': {
82
96
  padding: 0,
@@ -86,7 +100,7 @@ export const codeblockTheme = EditorView.theme({
86
100
  flexDirection: 'column',
87
101
  fontFamily: 'var(--cm-font-family)',
88
102
  boxShadow: '-12px 12px 1px rgba(0,0,0,0.3)',
89
- fontSize: '1rem',
103
+ fontSize: FS,
90
104
  maxWidth: 'min(calc(100% - 2rem), 62ch)',
91
105
  border: '2px solid var(--cm-tooltip-border)',
92
106
  overflow: 'auto',
@@ -126,7 +140,7 @@ export const codeblockTheme = EditorView.theme({
126
140
  '.documentation > *': {
127
141
  margin: 0,
128
142
  padding: '0.25rem 6px',
129
- fontSize: '1rem',
143
+ fontSize: FS,
130
144
  },
131
145
  '.documentation > p > code': {
132
146
  backgroundColor: 'var(--cm-comment-bg)',
@@ -154,16 +168,169 @@ export const codeblockTheme = EditorView.theme({
154
168
  padding: 0,
155
169
  background: 'var(--cm-toolbar-background)',
156
170
  fontFamily: 'var(--cm-font-family)',
157
- fontSize: '1rem',
171
+ fontSize: FS,
158
172
  listStyleType: 'none',
159
173
  width: '100%',
160
174
  maxHeight: '25vh',
161
175
  overflowY: 'auto',
176
+ zIndex: 200,
162
177
  },
163
178
  '.cm-gutters': {
164
179
  borderRight: 'none',
165
180
  },
166
181
  '.cm-panels-top': {
167
- borderBottom: 'none'
168
- }
182
+ borderBottom: 'none',
183
+ zIndex: 301,
184
+ },
185
+ // CSS border spinner for file loading indicator
186
+ '.cm-loading': {
187
+ display: 'inline-block',
188
+ width: FS,
189
+ height: FS,
190
+ border: '2px solid currentColor',
191
+ borderTopColor: 'transparent',
192
+ borderRadius: '50%',
193
+ boxSizing: 'border-box',
194
+ animation: 'cm-spin 0.8s linear infinite',
195
+ marginLeft: '4px'
196
+ },
197
+ '@keyframes cm-spin': {
198
+ '0%': { transform: 'rotate(0deg)' },
199
+ '100%': { transform: 'rotate(360deg)' },
200
+ },
201
+ // Settings cog + LSP log button in toolbar (far right)
202
+ '.cm-toolbar-settings-cog, .cm-toolbar-lsp-log': {
203
+ border: 'none',
204
+ background: 'transparent',
205
+ color: 'var(--cm-toolbar-color)',
206
+ cursor: 'pointer',
207
+ padding: '0 6px',
208
+ fontSize: FS_875,
209
+ lineHeight: 'inherit',
210
+ flexShrink: '0',
211
+ transition: 'transform 0.25s ease',
212
+ },
213
+ '.cm-toolbar-settings-cog.cm-cog-active': {
214
+ transform: 'rotate(90deg)',
215
+ },
216
+ // Settings / log overlay — anchored at top, grows downward
217
+ '.cm-settings-overlay': {
218
+ position: 'absolute',
219
+ top: 0,
220
+ left: 0,
221
+ right: 0,
222
+ overflowY: 'auto',
223
+ background: 'var(--cm-background)',
224
+ color: 'var(--cm-toolbar-color)',
225
+ zIndex: 1000,
226
+ fontFamily: 'var(--cm-font-family)',
227
+ fontSize: FS,
228
+ },
229
+ '.cm-settings-section': {
230
+ padding: '8px 6px',
231
+ },
232
+ '.cm-settings-section-title': {
233
+ fontWeight: 'bold',
234
+ marginBottom: '6px',
235
+ fontSize: FS_85,
236
+ opacity: '0.7',
237
+ },
238
+ '.cm-settings-row': {
239
+ display: 'flex',
240
+ alignItems: 'center',
241
+ marginBottom: '6px',
242
+ gap: '8px',
243
+ },
244
+ '.cm-settings-row > label': {
245
+ flex: '0 0 auto',
246
+ whiteSpace: 'nowrap',
247
+ },
248
+ '.cm-settings-control': {
249
+ display: 'flex',
250
+ alignItems: 'center',
251
+ gap: '4px',
252
+ },
253
+ // Fixed pixel width so font-size changes don't relayout the slider
254
+ '.cm-settings-font-size-range': {
255
+ width: '120px',
256
+ flexShrink: '0',
257
+ },
258
+ '.cm-settings-font-size-input': {
259
+ background: 'var(--cm-background)',
260
+ color: 'inherit',
261
+ border: '1px solid var(--cm-tooltip-border)',
262
+ borderRadius: '2px',
263
+ padding: '2px 4px',
264
+ fontSize: 'inherit',
265
+ fontFamily: 'var(--cm-font-family)',
266
+ width: '3em',
267
+ textAlign: 'right',
268
+ },
269
+ '.cm-settings-select': {
270
+ background: 'var(--cm-background)',
271
+ color: 'inherit',
272
+ border: '1px solid var(--cm-tooltip-border)',
273
+ borderRadius: '2px',
274
+ padding: '2px 4px',
275
+ fontSize: 'inherit',
276
+ fontFamily: 'var(--cm-font-family)',
277
+ },
278
+ '.cm-settings-radio-group': {
279
+ display: 'flex',
280
+ gap: '4px',
281
+ alignItems: 'center',
282
+ },
283
+ '.cm-settings-radio-group label': {
284
+ marginRight: '6px',
285
+ },
286
+ '.cm-settings-input': {
287
+ background: 'var(--cm-background)',
288
+ color: 'inherit',
289
+ border: '1px solid var(--cm-tooltip-border)',
290
+ borderRadius: '2px',
291
+ padding: '2px 6px',
292
+ fontSize: 'inherit',
293
+ fontFamily: 'var(--cm-font-family)',
294
+ flex: 1,
295
+ minWidth: 0,
296
+ },
297
+ '.cm-settings-button': {
298
+ background: 'var(--cm-background)',
299
+ color: 'inherit',
300
+ border: '1px solid var(--cm-tooltip-border)',
301
+ borderRadius: '2px',
302
+ padding: '4px 8px',
303
+ fontSize: 'inherit',
304
+ cursor: 'pointer',
305
+ },
306
+ '.cm-settings-button-disabled': {
307
+ opacity: '0.5',
308
+ cursor: 'not-allowed',
309
+ },
310
+ // LSP log content
311
+ '.cm-lsp-log-content': {
312
+ padding: '8px 12px',
313
+ fontFamily: 'var(--cm-font-family)',
314
+ fontSize: FS_75,
315
+ lineHeight: 1.5,
316
+ whiteSpace: 'pre-wrap',
317
+ wordBreak: 'break-all',
318
+ overflowY: 'auto',
319
+ flex: 1,
320
+ },
321
+ '.cm-lsp-log-entry': {
322
+ padding: '1px 0',
323
+ },
324
+ '.cm-lsp-log-error': {
325
+ color: 'var(--cm-diagnostic-error-bg)',
326
+ },
327
+ '.cm-lsp-log-warn': {
328
+ color: '#e5a100',
329
+ },
330
+ '.cm-lsp-log-info': {
331
+ opacity: '0.8',
332
+ },
333
+ '.cm-lsp-log-log': {
334
+ opacity: '0.6',
335
+ },
169
336
  });
@@ -113,7 +113,7 @@ const darkModeStyles = {
113
113
  "--cm-comment": "#6a9955",
114
114
  "--cm-link": "#4078f2",
115
115
  "--cm-invalid": "#ff0000",
116
- "--cm-search-result-color": "var(--cm-angle-bracket)",
116
+ "--cm-search-result-color": "var(--cm-foreground)",
117
117
  "--cm-search-result-color-hover": "#ffffff",
118
118
  "--cm-command-result-color": "var(--cm-search-result-color-hover)",
119
119
  "--cm-toolbar-background": "var(--cm-toolbar-bg-dark)",
@@ -129,6 +129,7 @@ export const vscodeStyleMod = new StyleModule({
129
129
  ":root, .cm-editor[data-theme='light'], [data-theme='light'] .cm-editor": {
130
130
  /* Shared */
131
131
  "--cm-font-family": 'Menlo, Monaco, Consolas, "Andale Mono", "Ubuntu Mono", "Courier New", monospace',
132
+ "--cm-icon-font-family": '"UbuntuMono NF", var(--cm-font-family)',
132
133
  /* Defaults to light theme */
133
134
  "--cm-background": "#ffffff",
134
135
  "--cm-foreground": "#383a42",
@@ -155,7 +156,7 @@ export const vscodeStyleMod = new StyleModule({
155
156
  "--cm-link": "#4078f2",
156
157
  "--cm-invalid": "#e45649",
157
158
  /* Additional UI colors */
158
- "--cm-search-result-color": "#838383",
159
+ "--cm-search-result-color": "var(--cm-foreground)",
159
160
  "--cm-search-result-color-hover": "var(--cm-toolbar-color)",
160
161
  "--cm-search-result-bg-hover": "#2490e94f",
161
162
  "--cm-search-result-color-selected": "#ffffff",
@@ -23,7 +23,19 @@ export declare namespace Vfs {
23
23
  export declare class VolarFs implements FileSystem {
24
24
  #private;
25
25
  constructor(fs: VfsInterface);
26
- stat(uri: URI): Promise<any>;
27
- readDirectory(uri: URI): Promise<[string, FileType][]>;
28
- readFile(uri: URI): Promise<string>;
26
+ /**
27
+ * Synchronously populate the cache from a pre-resolved map of path → content.
28
+ * This bypasses async VFS reads entirely, ensuring TypeScript gets lib files
29
+ * immediately on first program creation.
30
+ */
31
+ preloadFromMap(files: Record<string, string>): void;
32
+ stat(uri: URI): Promise<any> | {
33
+ type: FileType;
34
+ ctime: number;
35
+ mtime: number;
36
+ size: number;
37
+ };
38
+ readDirectory(uri: URI): [string, FileType][] | Promise<[string, FileType][]>;
39
+ readFile(uri: URI): string | Promise<string>;
40
+ getCacheSize(): number;
29
41
  }
package/dist/utils/fs.js CHANGED
@@ -58,7 +58,6 @@ export var Vfs;
58
58
  let cur = "/";
59
59
  for (const part of parts) {
60
60
  cur = cur === "/" ? `/${part}` : `${cur}/${part}`;
61
- console.log('creating', { cur, abs });
62
61
  const exists = await this.exists(cur);
63
62
  if (exists)
64
63
  continue;
@@ -257,8 +256,8 @@ export var Vfs;
257
256
  * for better performance with large files.
258
257
  */
259
258
  Vfs.worker = async (bufferOrUrl) => {
260
- const url = new URL('../workers/fs.worker.js', import.meta.url);
261
- const worker = new SharedWorker(url, { type: 'module' });
259
+ // TODO: fix this for non-Vite consumers
260
+ const worker = new SharedWorker(new URL('../workers/fs.worker.js', import.meta.url), { type: 'module' });
262
261
  worker.port.start();
263
262
  const proxy = Comlink.wrap(worker.port);
264
263
  let fs;
@@ -277,6 +276,7 @@ export var Vfs;
277
276
  // Buffer provided - use traditional mount method
278
277
  ({ fs } = await proxy.mount(Comlink.transfer({ buffer: bufferOrUrl, mountPoint: "/" }, [bufferOrUrl])));
279
278
  }
279
+ console.debug('Filesystem worker mounted');
280
280
  return Comlink.proxy(Vfs.fromMemfs(fs));
281
281
  };
282
282
  async function* walk(fs, path) {
@@ -295,16 +295,95 @@ export var Vfs;
295
295
  })(Vfs || (Vfs = {}));
296
296
  export class VolarFs {
297
297
  #fs;
298
+ #fileCache = new Map();
299
+ #statCache = new Map();
300
+ #dirCache = new Map();
298
301
  constructor(fs) {
299
302
  this.#fs = fs;
300
303
  }
301
- async stat(uri) {
304
+ /**
305
+ * Synchronously populate the cache from a pre-resolved map of path → content.
306
+ * This bypasses async VFS reads entirely, ensuring TypeScript gets lib files
307
+ * immediately on first program creation.
308
+ */
309
+ preloadFromMap(files) {
310
+ const now = Date.now();
311
+ for (const [path, content] of Object.entries(files)) {
312
+ this.#fileCache.set(path, content);
313
+ this.#statCache.set(path, {
314
+ type: FileType.File,
315
+ ctime: now,
316
+ mtime: now,
317
+ size: content.length,
318
+ });
319
+ }
320
+ // Build directory tree from file paths
321
+ const dirChildren = new Map();
322
+ for (const path of Object.keys(files)) {
323
+ let dir = path;
324
+ let child = '';
325
+ while (true) {
326
+ const lastSlash = dir.lastIndexOf('/');
327
+ if (lastSlash < 0)
328
+ break;
329
+ child = dir.substring(lastSlash + 1);
330
+ dir = dir.substring(0, lastSlash) || '/';
331
+ if (!dirChildren.has(dir)) {
332
+ dirChildren.set(dir, new Map());
333
+ }
334
+ const children = dirChildren.get(dir);
335
+ // First encounter of this child — it's the file itself
336
+ if (!children.has(child)) {
337
+ // If we've already seen this as a parent dir, it's a Directory
338
+ children.set(child, dirChildren.has(dir === '/' ? `/${child}` : `${dir}/${child}`) ? FileType.Directory : FileType.File);
339
+ }
340
+ if (dir === '/')
341
+ break;
342
+ }
343
+ }
344
+ // Update directory type for children that are actually directories
345
+ for (const [dirPath, children] of dirChildren) {
346
+ for (const [name] of children) {
347
+ const fullPath = dirPath === '/' ? `/${name}` : `${dirPath}/${name}`;
348
+ if (dirChildren.has(fullPath)) {
349
+ children.set(name, FileType.Directory);
350
+ }
351
+ }
352
+ }
353
+ // Cache directory listings and stats
354
+ for (const [dirPath, children] of dirChildren) {
355
+ this.#dirCache.set(dirPath, [...children.entries()]);
356
+ this.#statCache.set(dirPath, {
357
+ type: FileType.Directory,
358
+ ctime: now,
359
+ mtime: now,
360
+ size: 0,
361
+ });
362
+ }
363
+ }
364
+ stat(uri) {
365
+ const cached = this.#statCache.get(uri.path);
366
+ if (cached)
367
+ return cached;
302
368
  return this.#fs.stat(uri.path);
303
369
  }
304
- async readDirectory(uri) {
370
+ readDirectory(uri) {
371
+ // Only use dirCache for node_modules subtree (stable, preloaded).
372
+ // Root and user directories must go through live VFS to pick up new files.
373
+ if (uri.path.startsWith('/node_modules/')) {
374
+ const cached = this.#dirCache.get(uri.path);
375
+ if (cached)
376
+ return cached;
377
+ }
305
378
  return this.#fs.readDir(uri.path);
306
379
  }
307
- async readFile(uri) {
380
+ readFile(uri) {
381
+ const cached = this.#fileCache.get(uri.path);
382
+ if (cached !== undefined)
383
+ return cached;
308
384
  return this.#fs.readFile(uri.path);
309
385
  }
386
+ getCacheSize() {
387
+ return this.#fileCache.size;
388
+ }
310
389
  }
@@ -1,26 +1,37 @@
1
1
  import { VfsInterface } from "../types";
2
- import { LanguageServerClient } from "@marimo-team/codemirror-languageserver";
3
2
  import { Extension } from "@codemirror/state";
4
- import { LanguageServer } from "@volar/language-server";
5
- import { EditorView } from "@codemirror/view";
6
- export type LSPClientExtension = {
7
- client: LanguageServerClient;
8
- } & Extension;
3
+ export declare const FileChangeType: {
4
+ readonly Created: 1;
5
+ readonly Changed: 2;
6
+ readonly Deleted: 3;
7
+ };
8
+ export interface LspLogEntry {
9
+ timestamp: number;
10
+ level: 'error' | 'warn' | 'info' | 'log';
11
+ message: string;
12
+ }
13
+ export declare namespace LspLog {
14
+ function entries(): readonly LspLogEntry[];
15
+ function push(level: LspLogEntry['level'], message: string): void;
16
+ function clear(): void;
17
+ function subscribe(fn: () => void): () => void;
18
+ }
9
19
  export type ClientOptions = {
10
- view: EditorView;
11
20
  language: string;
12
21
  path: string;
13
22
  fs: VfsInterface;
23
+ libFiles?: Record<string, string>;
14
24
  };
15
- export declare const languageServerFactory: Map<string, (args: {
16
- fs: VfsInterface;
17
- }) => Promise<{
18
- server: LanguageServer;
19
- }>>;
20
25
  export declare const lspWorkers: Map<string, SharedWorker>;
21
26
  export declare namespace LSP {
22
- function worker(language: string, fs: VfsInterface): Promise<{
27
+ function worker(language: string, fs: VfsInterface, libFiles?: Record<string, string>): Promise<{
23
28
  worker: SharedWorker;
24
- }>;
25
- function client({ fs, language, path, view }: ClientOptions): Promise<LSPClientExtension>;
29
+ lspPort: MessagePort;
30
+ } | null>;
31
+ function client({ fs, language, path, libFiles }: ClientOptions): Promise<Extension | null>;
32
+ /**
33
+ * Notify all connected LSP clients that a file was created, changed, or deleted.
34
+ * This sends workspace/didChangeWatchedFiles so the server re-evaluates the project.
35
+ */
36
+ function notifyFileChanged(path: string, type?: number): void;
26
37
  }