@joinezco/codeblock 0.0.8 → 0.0.10

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 (73) hide show
  1. package/dist/editor.d.ts +30 -3
  2. package/dist/editor.js +416 -43
  3. package/dist/index.d.ts +5 -1
  4. package/dist/index.js +5 -1
  5. package/dist/lsps/index.d.ts +5 -0
  6. package/dist/lsps/index.js +9 -2
  7. package/dist/lsps/typescript.d.ts +3 -1
  8. package/dist/lsps/typescript.js +8 -17
  9. package/dist/panels/settings.d.ts +22 -0
  10. package/dist/panels/settings.js +267 -0
  11. package/dist/panels/terminal.d.ts +3 -0
  12. package/dist/panels/terminal.js +76 -0
  13. package/dist/panels/toolbar.d.ts +53 -3
  14. package/dist/panels/toolbar.js +1336 -164
  15. package/dist/panels/toolbar.test.js +20 -14
  16. package/dist/rpc/transport.d.ts +2 -11
  17. package/dist/rpc/transport.js +19 -35
  18. package/dist/themes/index.js +226 -13
  19. package/dist/themes/vscode.js +3 -2
  20. package/dist/types.d.ts +5 -0
  21. package/dist/utils/fs.d.ts +22 -3
  22. package/dist/utils/fs.js +126 -21
  23. package/dist/utils/lsp.d.ts +26 -15
  24. package/dist/utils/lsp.js +79 -44
  25. package/dist/utils/search.d.ts +2 -0
  26. package/dist/utils/search.js +13 -4
  27. package/dist/utils/typescript-defaults.d.ts +57 -0
  28. package/dist/utils/typescript-defaults.js +208 -0
  29. package/dist/utils/typescript-defaults.test.d.ts +1 -0
  30. package/dist/utils/typescript-defaults.test.js +197 -0
  31. package/dist/workers/fs.worker.d.ts +4 -8
  32. package/dist/workers/fs.worker.js +30 -60
  33. package/dist/workers/javascript.worker.js +11 -9
  34. package/package.json +8 -4
  35. package/dist/assets/clike-C8IJ2oj_.js +0 -1
  36. package/dist/assets/cmake-BQqOBYOt.js +0 -1
  37. package/dist/assets/dockerfile-C_y-rIpk.js +0 -1
  38. package/dist/assets/fs.worker-BwEqZcql.ts +0 -109
  39. package/dist/assets/go-CTD25R5P.js +0 -1
  40. package/dist/assets/haskell-BWDZoCOh.js +0 -1
  41. package/dist/assets/index-9HdhmM_Y.js +0 -1
  42. package/dist/assets/index-C-QhPFHP.js +0 -3
  43. package/dist/assets/index-C3BnE2cG.js +0 -222
  44. package/dist/assets/index-CGx5MZO7.js +0 -6
  45. package/dist/assets/index-CIuq3uTk.js +0 -1
  46. package/dist/assets/index-CXFONXS8.js +0 -1
  47. package/dist/assets/index-D5Z27j1C.js +0 -1
  48. package/dist/assets/index-DWOBdRjn.js +0 -1
  49. package/dist/assets/index-Dvu-FFzd.js +0 -1
  50. package/dist/assets/index-Dx_VuNNd.js +0 -1
  51. package/dist/assets/index-I0dlv-r3.js +0 -1
  52. package/dist/assets/index-MGle_v2x.js +0 -1
  53. package/dist/assets/index-N-GE7HTU.js +0 -1
  54. package/dist/assets/index-aEsF5o-7.js +0 -2
  55. package/dist/assets/index-as7ELo0J.js +0 -1
  56. package/dist/assets/index-gUUzXNuP.js +0 -1
  57. package/dist/assets/index-pGm0qkrJ.js +0 -13
  58. package/dist/assets/javascript.worker-C1zGArKk.js +0 -527
  59. package/dist/assets/lua-BgMRiT3U.js +0 -1
  60. package/dist/assets/perl-CdXCOZ3F.js +0 -1
  61. package/dist/assets/process-Dw9K5EnD.js +0 -1357
  62. package/dist/assets/properties-C78fOPTZ.js +0 -1
  63. package/dist/assets/ruby-B2Rjki9n.js +0 -1
  64. package/dist/assets/shell-CjFT_Tl9.js +0 -1
  65. package/dist/assets/swift-BzpIVaGY.js +0 -1
  66. package/dist/assets/toml-BXUEaScT.js +0 -1
  67. package/dist/assets/vb-CmGdzxic.js +0 -1
  68. package/dist/e2e/example.spec.d.ts +0 -5
  69. package/dist/e2e/example.spec.js +0 -44
  70. package/dist/index.html +0 -16
  71. package/dist/resources/config.json +0 -13
  72. package/dist/snapshot.bin +0 -0
  73. package/dist/styles.css +0 -7
package/dist/editor.d.ts CHANGED
@@ -4,7 +4,23 @@ 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
- export type { CommandResult } from "./panels/toolbar";
7
+ import { TypescriptDefaultsConfig } from "./utils/typescript-defaults";
8
+ import type { EditorSettings } from "./panels/settings";
9
+ export type { CommandResult, BrowseEntry } from "./panels/toolbar";
10
+ declare class FileChangeBus {
11
+ private listeners;
12
+ subscribe(path: string, view: EditorView, callback: (content: string) => void): () => void;
13
+ /** Notify all listeners for `path` except the source view. */
14
+ notify(path: string, content: string, sourceView: EditorView): void;
15
+ }
16
+ export declare const fileChangeBus: FileChangeBus;
17
+ type SettingsChangeCallback = (settings: Partial<import("./panels/settings").EditorSettings>) => void;
18
+ declare class SettingsChangeBus {
19
+ private listeners;
20
+ subscribe(view: EditorView, callback: SettingsChangeCallback): () => void;
21
+ notify(settings: Partial<import("./panels/settings").EditorSettings>, sourceView: EditorView): void;
22
+ }
23
+ export declare const settingsChangeBus: SettingsChangeBus;
8
24
  export type CodeblockConfig = {
9
25
  fs: VfsInterface;
10
26
  cwd?: string;
@@ -14,6 +30,11 @@ export type CodeblockConfig = {
14
30
  index?: SearchIndex;
15
31
  language?: ExtensionOrLanguage;
16
32
  dark?: boolean;
33
+ settings?: Partial<EditorSettings>;
34
+ typescript?: TypescriptDefaultsConfig & {
35
+ /** Resolves a TypeScript lib name (e.g. "es5") to its `.d.ts` file content */
36
+ resolveLib: (name: string) => Promise<string>;
37
+ };
17
38
  };
18
39
  export type CreateCodeblockArgs = CodeblockConfig & {
19
40
  parent: HTMLElement;
@@ -25,8 +46,13 @@ export declare const languageSupportCompartment: Compartment;
25
46
  export declare const languageServerCompartment: Compartment;
26
47
  export declare const indentationCompartment: Compartment;
27
48
  export declare const readOnlyCompartment: Compartment;
49
+ export declare const lineWrappingCompartment: Compartment;
50
+ export declare const terminalCompartment: Compartment;
51
+ export declare const lineNumbersCompartment: Compartment;
52
+ export declare const foldGutterCompartment: Compartment;
28
53
  export declare const openFileEffect: import("@codemirror/state").StateEffectType<{
29
54
  path: string;
55
+ skipSave?: boolean;
30
56
  }>;
31
57
  export declare const fileLoadedEffect: import("@codemirror/state").StateEffectType<{
32
58
  path: string;
@@ -36,6 +62,7 @@ export declare const fileLoadedEffect: import("@codemirror/state").StateEffectTy
36
62
  export declare const setThemeEffect: import("@codemirror/state").StateEffectType<{
37
63
  dark: boolean;
38
64
  }>;
65
+ export declare const toggleSvgPreviewEffect: import("@codemirror/state").StateEffectType<void>;
39
66
  export declare const currentFileField: StateField<{
40
67
  path: string | null;
41
68
  content: string;
@@ -43,11 +70,11 @@ export declare const currentFileField: StateField<{
43
70
  loading: boolean;
44
71
  }>;
45
72
  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<{
73
+ export declare const codeblock: ({ content, fs, cwd, filepath, language, toolbar, index, dark, settings, typescript }: CodeblockConfig) => (Extension | StateField<EditorSettings> | StateField<import("./panels/toolbar").SearchResult[]> | StateField<{
47
74
  path: string | null;
48
75
  content: string;
49
76
  language: ExtensionOrLanguage | null;
50
77
  loading: boolean;
51
78
  }>)[];
52
79
  export declare const basicSetup: Extension;
53
- export declare function createCodeblock({ parent, fs, filepath, language, content, cwd, toolbar, index, dark }: CreateCodeblockArgs): EditorView;
80
+ export declare function createCodeblock({ parent, fs, filepath, language, content, cwd, toolbar, index, dark, settings, typescript }: CreateCodeblockArgs): EditorView;
package/dist/editor.js CHANGED
@@ -9,13 +9,59 @@ 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
- import { lintKeymap } from "@codemirror/lint";
12
+ import { lintKeymap, setDiagnostics } from "@codemirror/lint";
14
13
  import { highlightCode } from "@lezer/highlight";
15
- import { LSP } from "./utils/lsp";
16
- import { toolbarPanel, searchResultsField } from "./panels/toolbar";
14
+ import { LSP, FileChangeType } from "./utils/lsp";
15
+ import { prefillTypescriptDefaults, getCachedLibFiles } from "./utils/typescript-defaults";
16
+ import { toolbarPanel, searchResultsField, registerFileAction } from "./panels/toolbar";
17
+ import { settingsField, updateSettingsEffect, resolveThemeDark, InitialSettingsFacet } from "./panels/settings";
17
18
  import { StyleModule } from "style-mod";
18
19
  import { dirname } from "path-browserify";
20
+ class FileChangeBus {
21
+ listeners = new Map();
22
+ subscribe(path, view, callback) {
23
+ let set = this.listeners.get(path);
24
+ if (!set) {
25
+ set = new Set();
26
+ this.listeners.set(path, set);
27
+ }
28
+ const listener = { view, callback };
29
+ set.add(listener);
30
+ return () => {
31
+ set.delete(listener);
32
+ if (set.size === 0)
33
+ this.listeners.delete(path);
34
+ };
35
+ }
36
+ /** Notify all listeners for `path` except the source view. */
37
+ notify(path, content, sourceView) {
38
+ const set = this.listeners.get(path);
39
+ if (!set)
40
+ return;
41
+ for (const listener of set) {
42
+ if (listener.view !== sourceView) {
43
+ listener.callback(content);
44
+ }
45
+ }
46
+ }
47
+ }
48
+ export const fileChangeBus = new FileChangeBus();
49
+ class SettingsChangeBus {
50
+ listeners = new Set();
51
+ subscribe(view, callback) {
52
+ const entry = { view, callback };
53
+ this.listeners.add(entry);
54
+ return () => this.listeners.delete(entry);
55
+ }
56
+ notify(settings, sourceView) {
57
+ for (const entry of this.listeners) {
58
+ if (entry.view !== sourceView) {
59
+ entry.callback(settings);
60
+ }
61
+ }
62
+ }
63
+ }
64
+ export const settingsChangeBus = new SettingsChangeBus();
19
65
  export const CodeblockFacet = Facet.define({
20
66
  combine: (values) => values[0]
21
67
  });
@@ -25,11 +71,17 @@ export const languageSupportCompartment = new Compartment();
25
71
  export const languageServerCompartment = new Compartment();
26
72
  export const indentationCompartment = new Compartment();
27
73
  export const readOnlyCompartment = new Compartment();
74
+ export const lineWrappingCompartment = new Compartment();
75
+ export const terminalCompartment = new Compartment();
76
+ export const lineNumbersCompartment = new Compartment();
77
+ export const foldGutterCompartment = new Compartment();
28
78
  // Effects + Fields for async file handling
29
79
  export const openFileEffect = StateEffect.define();
30
80
  export const fileLoadedEffect = StateEffect.define();
31
81
  // Light mode/dark mode theme toggle
32
82
  export const setThemeEffect = StateEffect.define();
83
+ // SVG preview toggle
84
+ export const toggleSvgPreviewEffect = StateEffect.define();
33
85
  // Holds the current file lifecycle
34
86
  export const currentFileField = StateField.define({
35
87
  create(state) {
@@ -95,43 +147,242 @@ export const renderMarkdownCode = (code, parser, highlighter) => {
95
147
  return result.getHTML();
96
148
  };
97
149
  // 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
- currentFileField,
101
- languageSupportCompartment.of([]),
102
- languageServerCompartment.of([]),
103
- indentationCompartment.of(indentUnit.of(" ")),
104
- readOnlyCompartment.of(EditorState.readOnly.of(false)),
105
- tooltips({ position: "fixed" }),
106
- showPanel.of(toolbar ? toolbarPanel : null),
107
- codeblockTheme,
108
- codeblockView,
109
- keymap.of(navigationKeymap.concat([indentWithTab])),
110
- vscodeLightDark,
111
- searchResultsField,
112
- ];
150
+ export const codeblock = ({ content, fs, cwd, filepath, language, toolbar = true, index, dark, settings, typescript }) => {
151
+ // Merge dark flag into initial settings for backward compat
152
+ const resolvedSettings = { ...settings };
153
+ if (dark !== undefined && !('theme' in resolvedSettings)) {
154
+ resolvedSettings.theme = dark ? 'dark' : 'light';
155
+ }
156
+ const showLineNums = resolvedSettings.showLineNumbers !== false; // default true
157
+ const showFold = resolvedSettings.showFoldGutter !== false; // default true
158
+ return [
159
+ configCompartment.of(CodeblockFacet.of({ content, fs, filepath, cwd, language, toolbar, index, dark, settings, typescript })),
160
+ InitialSettingsFacet.of(resolvedSettings),
161
+ currentFileField,
162
+ languageSupportCompartment.of([]),
163
+ languageServerCompartment.of([]),
164
+ indentationCompartment.of(indentUnit.of(" ")),
165
+ readOnlyCompartment.of(EditorState.readOnly.of(false)),
166
+ lineWrappingCompartment.of([]),
167
+ terminalCompartment.of([]),
168
+ lineNumbersCompartment.of(showLineNums ? [lineNumbers(), highlightActiveLineGutter()] : []),
169
+ foldGutterCompartment.of(showFold ? [foldGutter()] : []),
170
+ tooltips({ position: "fixed" }),
171
+ showPanel.of(toolbar ? toolbarPanel : null),
172
+ settingsField,
173
+ codeblockTheme,
174
+ codeblockView,
175
+ keymap.of(navigationKeymap.concat([indentWithTab])),
176
+ vscodeLightDark,
177
+ searchResultsField,
178
+ ];
179
+ };
113
180
  // ViewPlugin reacts to field state & effects, with microtask scheduling to avoid nested updates
181
+ // Inject @font-face for Nerd Font icons (idempotent)
182
+ let nerdFontInjected = false;
183
+ function injectNerdFontFace() {
184
+ if (nerdFontInjected)
185
+ return;
186
+ nerdFontInjected = true;
187
+ const style = document.createElement('style');
188
+ style.textContent = `@font-face {
189
+ font-family: 'UbuntuMono NF';
190
+ src: url('/fonts/UbuntuMonoNerdFont-Regular.ttf') format('truetype');
191
+ font-weight: normal;
192
+ font-style: normal;
193
+ font-display: swap;
194
+ }`;
195
+ document.head.appendChild(style);
196
+ }
114
197
  const codeblockView = ViewPlugin.define((view) => {
115
198
  StyleModule.mount(document, vscodeStyleMod);
199
+ injectNerdFontFace();
116
200
  let { fs } = view.state.facet(CodeblockFacet);
201
+ // Flag to suppress save when receiving external file updates
202
+ let receivingExternalUpdate = false;
203
+ // Flag to suppress re-broadcast when receiving settings from another editor
204
+ let receivingExternalSettings = false;
205
+ // Subscription cleanup for file change notifications
206
+ let unsubscribeFileChanges = null;
207
+ // Subscribe to settings changes from other editors
208
+ const unsubscribeSettings = settingsChangeBus.subscribe(view, (partial) => {
209
+ receivingExternalSettings = true;
210
+ try {
211
+ const effects = [updateSettingsEffect.of(partial)];
212
+ if ('theme' in partial && partial.theme) {
213
+ effects.push(setThemeEffect.of({ dark: resolveThemeDark(partial.theme) }));
214
+ }
215
+ if ('lineWrap' in partial) {
216
+ effects.push(lineWrappingCompartment.reconfigure(partial.lineWrap ? EditorView.lineWrapping : []));
217
+ }
218
+ if ('showLineNumbers' in partial) {
219
+ effects.push(lineNumbersCompartment.reconfigure(partial.showLineNumbers ? [lineNumbers(), highlightActiveLineGutter()] : []));
220
+ }
221
+ if ('showFoldGutter' in partial) {
222
+ effects.push(foldGutterCompartment.reconfigure(partial.showFoldGutter ? [foldGutter()] : []));
223
+ }
224
+ // autoHideToolbar is handled by the toolbar panel's JS event handlers,
225
+ // not CSS classes — the updateSettingsEffect propagation is sufficient.
226
+ view.dispatch({ effects });
227
+ }
228
+ finally {
229
+ receivingExternalSettings = false;
230
+ }
231
+ });
117
232
  // Debounced save
118
233
  const save = debounce(async () => {
119
234
  const fileState = view.state.field(currentFileField);
120
235
  if (fileState.path) {
236
+ const content = view.state.doc.toString();
121
237
  // confirm parent exists
122
238
  const parent = dirname(fileState.path);
123
239
  if (parent) {
124
240
  await fs.mkdir(parent, { recursive: true }).catch(console.error);
125
241
  }
126
- await fs.writeFile(fileState.path, view.state.doc.toString()).catch(console.error);
242
+ await fs.writeFile(fileState.path, content).catch(console.error);
243
+ LSP.notifyFileChanged(fileState.path, FileChangeType.Changed);
244
+ // Notify other views of the same file
245
+ fileChangeBus.notify(fileState.path, content, view);
127
246
  }
128
247
  }, 500);
248
+ // Subscribe to external file changes for the given path
249
+ function subscribeToFileChanges(path) {
250
+ // Unsubscribe from previous file
251
+ if (unsubscribeFileChanges) {
252
+ unsubscribeFileChanges();
253
+ unsubscribeFileChanges = null;
254
+ }
255
+ unsubscribeFileChanges = fileChangeBus.subscribe(path, view, (newContent) => {
256
+ const currentContent = view.state.doc.toString();
257
+ if (newContent === currentContent)
258
+ return; // No change
259
+ receivingExternalUpdate = true;
260
+ try {
261
+ view.dispatch({
262
+ changes: { from: 0, to: view.state.doc.length, insert: newContent }
263
+ });
264
+ }
265
+ finally {
266
+ receivingExternalUpdate = false;
267
+ }
268
+ });
269
+ }
129
270
  // Guard to prevent duplicate opens for same path while loading
130
271
  let opening = null;
272
+ // Track the path of the currently loaded file for correct save-on-switch.
273
+ // Only set AFTER a file has actually been loaded (not during initial loading state).
274
+ const initialFile = view.state.field(currentFileField);
275
+ let activePath = (initialFile.loading) ? null : initialFile.path;
276
+ // Preview element for images/SVGs
277
+ let previewEl = null;
278
+ let svgViewMode = 'preview';
279
+ const IMAGE_EXTENSIONS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'ico', 'avif']);
280
+ const SVG_EXTENSION = 'svg';
281
+ function hideScroller() {
282
+ const scroller = view.dom.querySelector('.cm-scroller');
283
+ if (scroller) {
284
+ // CodeMirror sets `display: flex !important` on .cm-scroller,
285
+ // so we can't use display:none. Hide via collapse instead.
286
+ scroller.style.visibility = 'hidden';
287
+ scroller.style.height = '0';
288
+ scroller.style.overflow = 'hidden';
289
+ scroller.style.position = 'absolute';
290
+ }
291
+ }
292
+ function showScroller() {
293
+ const scroller = view.dom.querySelector('.cm-scroller');
294
+ if (scroller) {
295
+ scroller.style.visibility = '';
296
+ scroller.style.height = '';
297
+ scroller.style.overflow = '';
298
+ scroller.style.position = '';
299
+ }
300
+ }
301
+ function removePreview() {
302
+ if (previewEl) {
303
+ previewEl.remove();
304
+ previewEl = null;
305
+ }
306
+ svgViewMode = 'preview';
307
+ showScroller();
308
+ }
309
+ function showImagePreview(content) {
310
+ removePreview();
311
+ previewEl = document.createElement('div');
312
+ previewEl.className = 'cm-image-preview';
313
+ previewEl.style.cssText = 'display:flex;align-items:center;justify-content:center;padding:16px;min-height:200px;overflow:auto;background:var(--cm-background, #1e1e1e);';
314
+ const img = document.createElement('img');
315
+ if (content.startsWith('data:') || content.startsWith('http') || content.startsWith('blob:')) {
316
+ img.src = content;
317
+ }
318
+ else {
319
+ const msg = document.createElement('div');
320
+ msg.style.cssText = 'color:var(--cm-toolbar-color, #ccc);text-align:center;';
321
+ msg.textContent = 'Image preview unavailable (import from disk to view)';
322
+ previewEl.appendChild(msg);
323
+ view.dom.appendChild(previewEl);
324
+ return;
325
+ }
326
+ img.style.maxWidth = '100%';
327
+ img.style.maxHeight = '400px';
328
+ img.style.objectFit = 'contain';
329
+ previewEl.appendChild(img);
330
+ hideScroller();
331
+ view.dom.appendChild(previewEl);
332
+ }
333
+ function renderSvgInto(container, content) {
334
+ container.innerHTML = '';
335
+ try {
336
+ const parser = new DOMParser();
337
+ const doc = parser.parseFromString(content, 'image/svg+xml');
338
+ const svgEl = doc.documentElement;
339
+ if (svgEl.tagName === 'svg') {
340
+ svgEl.style.maxWidth = '100%';
341
+ svgEl.style.maxHeight = '300px';
342
+ svgEl.removeAttribute('width');
343
+ svgEl.removeAttribute('height');
344
+ container.appendChild(document.importNode(svgEl, true));
345
+ }
346
+ else {
347
+ container.textContent = 'Invalid SVG';
348
+ container.style.color = 'var(--cm-toolbar-color, #ccc)';
349
+ }
350
+ }
351
+ catch {
352
+ container.textContent = 'SVG parse error';
353
+ container.style.color = 'var(--cm-toolbar-color, #ccc)';
354
+ }
355
+ }
356
+ function showSvgView(content, mode) {
357
+ removePreview();
358
+ svgViewMode = mode;
359
+ previewEl = document.createElement('div');
360
+ previewEl.className = 'cm-svg-preview';
361
+ if (mode === 'preview') {
362
+ // Preview mode: hide editor, show rendered SVG
363
+ hideScroller();
364
+ previewEl.style.cssText = 'padding:16px;display:flex;align-items:center;justify-content:center;overflow:auto;background:var(--cm-background, #1e1e1e);min-height:200px;';
365
+ renderSvgInto(previewEl, content);
366
+ }
367
+ else {
368
+ // Source mode: show editor, hide preview
369
+ showScroller();
370
+ previewEl.style.display = 'none';
371
+ }
372
+ view.dom.appendChild(previewEl);
373
+ }
374
+ function updateSvgPreview() {
375
+ if (!previewEl || !previewEl.classList.contains('cm-svg-preview') || previewEl.style.display === 'none')
376
+ return;
377
+ renderSvgInto(previewEl, view.state.doc.toString());
378
+ }
131
379
  async function setLanguageSupport(language) {
132
380
  if (!language)
133
381
  return;
134
- const langSupport = await getLanguageSupport(extOrLanguageToLanguageId[language]);
382
+ const langSupport = await getLanguageSupport(extOrLanguageToLanguageId[language]).catch((e) => {
383
+ console.error(`Failed to load language support for ${language}`, e);
384
+ return null;
385
+ });
135
386
  safeDispatch(view, {
136
387
  effects: [
137
388
  languageSupportCompartment.reconfigure(langSupport || []),
@@ -144,10 +395,28 @@ const codeblockView = ViewPlugin.define((view) => {
144
395
  if (opening === path)
145
396
  return;
146
397
  opening = path;
398
+ // Cancel the debounced save and manually flush the current file.
399
+ // We can't use save.flush() because openFileEffect has already updated
400
+ // currentFileField.path to the NEW path, but the document still holds
401
+ // the OLD file's content. Using activePath ensures we write to the
402
+ // correct location.
403
+ save.cancel();
404
+ if (activePath && view.state.field(settingsField).autosave) {
405
+ const oldPath = activePath;
406
+ const oldContent = view.state.doc.toString();
407
+ const parent = dirname(oldPath);
408
+ if (parent)
409
+ await fs.mkdir(parent, { recursive: true }).catch(console.error);
410
+ await fs.writeFile(oldPath, oldContent).catch(console.error);
411
+ LSP.notifyFileChanged(oldPath, FileChangeType.Changed);
412
+ }
147
413
  try {
148
414
  const ext = path.split('.').pop()?.toLowerCase();
149
415
  const lang = (ext ? (extOrLanguageToLanguageId)[ext] ?? null : language) || 'markdown';
150
- let langSupport = lang ? await getLanguageSupport(lang) : null;
416
+ let langSupport = lang ? await getLanguageSupport(lang).catch((e) => {
417
+ console.error(`Failed to load language support for ${lang}`, e);
418
+ return null;
419
+ }) : null;
151
420
  safeDispatch(view, {
152
421
  effects: [
153
422
  languageSupportCompartment.reconfigure(langSupport || []),
@@ -155,20 +424,78 @@ const codeblockView = ViewPlugin.define((view) => {
155
424
  });
156
425
  const exists = await fs.exists(path);
157
426
  const content = exists ? await fs.readFile(path) : "";
427
+ // Ensure the file exists on VFS before LSP initialization.
428
+ // The LSP uses readDirectory to find source files and match them
429
+ // against tsconfig. If the file doesn't exist yet, Volar falls
430
+ // back to an inferred project that lacks lib file configuration.
431
+ if (!exists) {
432
+ await fs.mkdir(dirname(path), { recursive: true }).catch(() => { });
433
+ await fs.writeFile(path, content);
434
+ LSP.notifyFileChanged(path, FileChangeType.Created);
435
+ }
436
+ // Add new files to the search index so they appear in future searches
437
+ const { index } = view.state.facet(CodeblockFacet);
438
+ if (index) {
439
+ index.add(path);
440
+ if (index.savePath)
441
+ index.save(fs, index.savePath);
442
+ }
158
443
  const unit = detectIndentationUnit(content) || " ";
159
- let lsp = lang ? await LSP.client({ view, language: lang, path, fs }) : null;
160
- safeDispatch(view, {
161
- changes: { from: 0, to: view.state.doc.length, insert: content },
162
- effects: [
163
- indentationCompartment.reconfigure(indentUnit.of(unit)),
164
- fileLoadedEffect.of({ path, content, language: lang }),
165
- languageServerCompartment.reconfigure([
166
- documentUri.of(`file:///${path}`),
167
- languageId.of(lang || ""),
168
- ...(lsp ? [lsp] : [])
169
- ]),
170
- ]
171
- });
444
+ // Lazily pre-fill TypeScript lib definitions when a TS/JS file is first opened
445
+ const tsExtensions = ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs', 'mts', 'cts'];
446
+ const { typescript } = view.state.facet(CodeblockFacet);
447
+ let libFiles;
448
+ if (typescript?.resolveLib && ext && tsExtensions.includes(ext)) {
449
+ libFiles = await prefillTypescriptDefaults(fs, typescript.resolveLib, typescript);
450
+ }
451
+ else {
452
+ libFiles = getCachedLibFiles();
453
+ }
454
+ let lsp = null;
455
+ if (lang) {
456
+ try {
457
+ lsp = await LSP.client({ language: lang, path, fs, libFiles });
458
+ }
459
+ catch (lspErr) {
460
+ // Gracefully degrade when LSP is unavailable (e.g. missing worker, test environment)
461
+ console.warn("LSP unavailable for this view:", lspErr);
462
+ }
463
+ }
464
+ activePath = path;
465
+ // Check for image/SVG files
466
+ const isRasterImage = ext ? IMAGE_EXTENSIONS.has(ext) : false;
467
+ const isSvg = ext === SVG_EXTENSION;
468
+ // Clear stale diagnostics from previous file
469
+ safeDispatch(view, setDiagnostics(view.state, []));
470
+ if (isRasterImage) {
471
+ // Raster image: show preview, hide editor content
472
+ safeDispatch(view, {
473
+ changes: { from: 0, to: view.state.doc.length, insert: content },
474
+ effects: [
475
+ fileLoadedEffect.of({ path, content, language: null }),
476
+ readOnlyCompartment.reconfigure(EditorState.readOnly.of(true)),
477
+ ]
478
+ });
479
+ showImagePreview(content);
480
+ }
481
+ else {
482
+ // Remove any existing preview
483
+ removePreview();
484
+ safeDispatch(view, {
485
+ changes: { from: 0, to: view.state.doc.length, insert: content },
486
+ effects: [
487
+ indentationCompartment.reconfigure(indentUnit.of(unit)),
488
+ fileLoadedEffect.of({ path, content, language: lang }),
489
+ languageServerCompartment.reconfigure(lsp ? [lsp] : []),
490
+ ]
491
+ });
492
+ // SVG: show live preview below the editor
493
+ if (isSvg) {
494
+ showSvgView(content, 'preview');
495
+ }
496
+ }
497
+ // Subscribe to changes from other views of the same file
498
+ subscribeToFileChanges(path);
172
499
  }
173
500
  catch (e) {
174
501
  console.error("Failed to open file", e);
@@ -189,12 +516,25 @@ const codeblockView = ViewPlugin.define((view) => {
189
516
  update(u) {
190
517
  // React to explicit openFileEffect requests
191
518
  for (let e of u.transactions.flatMap(t => t.effects)) {
192
- if (e.is(openFileEffect))
519
+ if (e.is(openFileEffect)) {
520
+ // Cancel debounced save immediately to prevent it from writing
521
+ // the old document content to the wrong (new) file path
522
+ save.cancel();
523
+ if (e.value.skipSave) {
524
+ // Caller already handled file operations (e.g. rename) —
525
+ // clear activePath so handleOpen won't save-on-switch
526
+ activePath = null;
527
+ }
193
528
  queueMicrotask(() => handleOpen(e.value.path));
529
+ }
194
530
  if (e.is(setThemeEffect)) {
195
531
  const dark = e.value.dark;
196
532
  u.view.dom.setAttribute('data-theme', dark ? 'dark' : 'light');
197
533
  }
534
+ if (e.is(toggleSvgPreviewEffect)) {
535
+ const newMode = svgViewMode === 'preview' ? 'source' : 'preview';
536
+ showSvgView(view.state.doc.toString(), newMode);
537
+ }
198
538
  }
199
539
  // Keep read-only in sync with loading state without dispatching new transactions
200
540
  const prev = u.startState.field(currentFileField);
@@ -203,21 +543,46 @@ const codeblockView = ViewPlugin.define((view) => {
203
543
  // Reconfigure readOnly via compartment inside the same update when possible
204
544
  safeDispatch(view, { effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(next.loading)) });
205
545
  }
206
- if (u.docChanged)
546
+ if (u.docChanged && !receivingExternalUpdate && u.state.field(settingsField).autosave)
207
547
  save();
548
+ // Live SVG preview update
549
+ if (u.docChanged && previewEl?.classList.contains('cm-svg-preview')) {
550
+ updateSvgPreview();
551
+ }
552
+ // Broadcast settings changes to other editors (unless we received them externally)
553
+ const prevSettings = u.startState.field(settingsField);
554
+ const nextSettings = u.state.field(settingsField);
555
+ if (prevSettings !== nextSettings && !receivingExternalSettings) {
556
+ // Compute the diff
557
+ const diff = {};
558
+ for (const key of Object.keys(nextSettings)) {
559
+ if (prevSettings[key] !== nextSettings[key]) {
560
+ diff[key] = nextSettings[key];
561
+ }
562
+ }
563
+ if (Object.keys(diff).length > 0) {
564
+ settingsChangeBus.notify(diff, view);
565
+ }
566
+ }
208
567
  // If fs changed via facet reconfig, refresh handle references
209
568
  const newFs = u.state.facet(CodeblockFacet).fs;
210
569
  if (fs !== newFs)
211
570
  fs = newFs;
571
+ },
572
+ destroy() {
573
+ if (unsubscribeFileChanges) {
574
+ unsubscribeFileChanges();
575
+ unsubscribeFileChanges = null;
576
+ }
577
+ unsubscribeSettings();
578
+ removePreview();
579
+ save.cancel();
212
580
  }
213
581
  };
214
582
  });
215
583
  export const basicSetup = (() => [
216
- lineNumbers(),
217
- highlightActiveLineGutter(),
218
584
  highlightSpecialChars(),
219
585
  history(),
220
- foldGutter(),
221
586
  drawSelection(),
222
587
  dropCursor(),
223
588
  EditorState.allowMultipleSelections.of(true),
@@ -239,10 +604,18 @@ export const basicSetup = (() => [
239
604
  ...lintKeymap
240
605
  ])
241
606
  ])();
242
- export function createCodeblock({ parent, fs, filepath, language, content = '', cwd = '/', toolbar = true, index, dark }) {
607
+ export function createCodeblock({ parent, fs, filepath, language, content = '', cwd = '/', toolbar = true, index, dark, settings, typescript }) {
243
608
  const state = EditorState.create({
244
609
  doc: content,
245
- extensions: [basicSetup, codeblock({ content, fs, filepath, cwd, language, toolbar, index, dark })]
610
+ extensions: [basicSetup, codeblock({ content, fs, filepath, cwd, language, toolbar, index, dark, settings, typescript })]
246
611
  });
247
- return new EditorView({ state, parent });
612
+ const view = new EditorView({ state, parent });
613
+ return view;
248
614
  }
615
+ // --- File-extension-specific toolbar commands ---
616
+ registerFileAction({
617
+ extensions: ['svg'],
618
+ label: 'SVG > Toggle preview',
619
+ icon: '\udb82\ude1b', // nf-md-image_outline (󰈛)
620
+ action: (view) => view.dispatch({ effects: toggleSvgPreviewEffect.of(undefined) }),
621
+ });
package/dist/index.d.ts CHANGED
@@ -1,6 +1,10 @@
1
- export { createCodeblock, codeblock, basicSetup, type CodeblockConfig, CodeblockFacet, setThemeEffect } from "./editor";
1
+ export { createCodeblock, codeblock, basicSetup, type CodeblockConfig, CodeblockFacet, setThemeEffect, fileChangeBus, settingsChangeBus, lineNumbersCompartment, foldGutterCompartment, toggleSvgPreviewEffect } from "./editor";
2
+ export { settingsField, updateSettingsEffect, InitialSettingsFacet, type EditorSettings } from "./panels/settings";
3
+ export { registerFileAction, type FileActionEntry } from "./panels/toolbar";
4
+ export { LspLog, type LspLogEntry } from "./utils/lsp";
2
5
  export { Vfs as CodeblockFS } from './utils/fs';
3
6
  export * from './utils/snapshot';
4
7
  export * from './types';
5
8
  export * from './utils/search';
6
9
  export * from './lsps';
10
+ export { prefillTypescriptDefaults, getCachedLibFiles, getRequiredLibs, getLibFieldForTarget, type TypescriptDefaultsConfig } from './utils/typescript-defaults';
package/dist/index.js CHANGED
@@ -1,6 +1,10 @@
1
- export { createCodeblock, codeblock, basicSetup, CodeblockFacet, setThemeEffect } from "./editor";
1
+ export { createCodeblock, codeblock, basicSetup, CodeblockFacet, setThemeEffect, fileChangeBus, settingsChangeBus, lineNumbersCompartment, foldGutterCompartment, toggleSvgPreviewEffect } from "./editor";
2
+ export { settingsField, updateSettingsEffect, InitialSettingsFacet } from "./panels/settings";
3
+ export { registerFileAction } from "./panels/toolbar";
4
+ export { LspLog } from "./utils/lsp";
2
5
  export { Vfs as CodeblockFS } from './utils/fs';
3
6
  export * from './utils/snapshot';
4
7
  export * from './types';
5
8
  export * from './utils/search';
6
9
  export * from './lsps';
10
+ export { prefillTypescriptDefaults, getCachedLibFiles, getRequiredLibs, getLibFieldForTarget } from './utils/typescript-defaults';