@joinezco/codeblock 0.0.9 → 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 (59) hide show
  1. package/dist/editor.d.ts +23 -2
  2. package/dist/editor.js +346 -41
  3. package/dist/index.d.ts +3 -2
  4. package/dist/index.js +3 -2
  5. package/dist/lsps/index.d.ts +5 -0
  6. package/dist/lsps/index.js +9 -2
  7. package/dist/panels/{footer.d.ts → settings.d.ts} +7 -1
  8. package/dist/panels/{footer.js → settings.js} +12 -3
  9. package/dist/panels/terminal.d.ts +3 -0
  10. package/dist/panels/terminal.js +76 -0
  11. package/dist/panels/toolbar.d.ts +40 -3
  12. package/dist/panels/toolbar.js +919 -160
  13. package/dist/themes/index.js +63 -17
  14. package/dist/types.d.ts +5 -0
  15. package/dist/utils/fs.d.ts +7 -0
  16. package/dist/utils/fs.js +41 -15
  17. package/dist/workers/fs.worker.d.ts +4 -8
  18. package/dist/workers/fs.worker.js +27 -53
  19. package/package.json +6 -3
  20. package/dist/assets/clike-C8IJ2oj_.js +0 -1
  21. package/dist/assets/cmake-BQqOBYOt.js +0 -1
  22. package/dist/assets/dockerfile-C_y-rIpk.js +0 -1
  23. package/dist/assets/fs.worker-DfanUHpQ.js +0 -21
  24. package/dist/assets/go-CTD25R5P.js +0 -1
  25. package/dist/assets/haskell-BWDZoCOh.js +0 -1
  26. package/dist/assets/index-BAnLzvMk.js +0 -1
  27. package/dist/assets/index-BBC9WDX6.js +0 -1
  28. package/dist/assets/index-BEXYxRro.js +0 -1
  29. package/dist/assets/index-BfYmUKH9.js +0 -13
  30. package/dist/assets/index-BhaTNAWE.js +0 -1
  31. package/dist/assets/index-CCbYDSng.js +0 -1
  32. package/dist/assets/index-CIi8tLT6.js +0 -1
  33. package/dist/assets/index-CaANcgI2.js +0 -3
  34. package/dist/assets/index-CkWzFNzm.js +0 -208
  35. package/dist/assets/index-D_XGv9QZ.js +0 -1
  36. package/dist/assets/index-DkmiPfkD.js +0 -1
  37. package/dist/assets/index-DmNlLMQ4.js +0 -6
  38. package/dist/assets/index-DmX_vI7D.js +0 -1
  39. package/dist/assets/index-DogEEevD.js +0 -1
  40. package/dist/assets/index-DsDl5qZV.js +0 -2
  41. package/dist/assets/index-gAy5mDg-.js +0 -1
  42. package/dist/assets/index-i5qJLB2h.js +0 -1
  43. package/dist/assets/javascript.worker-ClsyHOLi.js +0 -552
  44. package/dist/assets/lua-BgMRiT3U.js +0 -1
  45. package/dist/assets/perl-CdXCOZ3F.js +0 -1
  46. package/dist/assets/process-Dw9K5EnD.js +0 -1357
  47. package/dist/assets/properties-C78fOPTZ.js +0 -1
  48. package/dist/assets/ruby-B2Rjki9n.js +0 -1
  49. package/dist/assets/shell-CjFT_Tl9.js +0 -1
  50. package/dist/assets/swift-BzpIVaGY.js +0 -1
  51. package/dist/assets/toml-BXUEaScT.js +0 -1
  52. package/dist/assets/vb-CmGdzxic.js +0 -1
  53. package/dist/e2e/editor.spec.d.ts +0 -1
  54. package/dist/e2e/editor.spec.js +0 -309
  55. package/dist/e2e/example.spec.d.ts +0 -5
  56. package/dist/e2e/example.spec.js +0 -44
  57. package/dist/index.html +0 -15
  58. package/dist/resources/config.json +0 -13
  59. package/dist/styles.css +0 -7
package/dist/editor.d.ts CHANGED
@@ -5,7 +5,22 @@ 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
+ import type { EditorSettings } from "./panels/settings";
8
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;
9
24
  export type CodeblockConfig = {
10
25
  fs: VfsInterface;
11
26
  cwd?: string;
@@ -15,6 +30,7 @@ export type CodeblockConfig = {
15
30
  index?: SearchIndex;
16
31
  language?: ExtensionOrLanguage;
17
32
  dark?: boolean;
33
+ settings?: Partial<EditorSettings>;
18
34
  typescript?: TypescriptDefaultsConfig & {
19
35
  /** Resolves a TypeScript lib name (e.g. "es5") to its `.d.ts` file content */
20
36
  resolveLib: (name: string) => Promise<string>;
@@ -31,8 +47,12 @@ export declare const languageServerCompartment: Compartment;
31
47
  export declare const indentationCompartment: Compartment;
32
48
  export declare const readOnlyCompartment: Compartment;
33
49
  export declare const lineWrappingCompartment: Compartment;
50
+ export declare const terminalCompartment: Compartment;
51
+ export declare const lineNumbersCompartment: Compartment;
52
+ export declare const foldGutterCompartment: Compartment;
34
53
  export declare const openFileEffect: import("@codemirror/state").StateEffectType<{
35
54
  path: string;
55
+ skipSave?: boolean;
36
56
  }>;
37
57
  export declare const fileLoadedEffect: import("@codemirror/state").StateEffectType<{
38
58
  path: string;
@@ -42,6 +62,7 @@ export declare const fileLoadedEffect: import("@codemirror/state").StateEffectTy
42
62
  export declare const setThemeEffect: import("@codemirror/state").StateEffectType<{
43
63
  dark: boolean;
44
64
  }>;
65
+ export declare const toggleSvgPreviewEffect: import("@codemirror/state").StateEffectType<void>;
45
66
  export declare const currentFileField: StateField<{
46
67
  path: string | null;
47
68
  content: string;
@@ -49,11 +70,11 @@ export declare const currentFileField: StateField<{
49
70
  loading: boolean;
50
71
  }>;
51
72
  export declare const renderMarkdownCode: (code: any, parser: any, highlighter: HighlightStyle) => string;
52
- export declare const codeblock: ({ content, fs, cwd, filepath, language, toolbar, index, typescript }: CodeblockConfig) => (Extension | StateField<import(".").EditorSettings> | 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<{
53
74
  path: string | null;
54
75
  content: string;
55
76
  language: ExtensionOrLanguage | null;
56
77
  loading: boolean;
57
78
  }>)[];
58
79
  export declare const basicSetup: Extension;
59
- export declare function createCodeblock({ parent, fs, filepath, language, content, cwd, toolbar, index, dark, typescript }: 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,14 +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 { lintKeymap } from "@codemirror/lint";
12
+ import { lintKeymap, setDiagnostics } from "@codemirror/lint";
13
13
  import { highlightCode } from "@lezer/highlight";
14
14
  import { LSP, FileChangeType } from "./utils/lsp";
15
15
  import { prefillTypescriptDefaults, getCachedLibFiles } from "./utils/typescript-defaults";
16
- import { toolbarPanel, searchResultsField } from "./panels/toolbar";
17
- import { settingsField } from "./panels/footer";
16
+ import { toolbarPanel, searchResultsField, registerFileAction } from "./panels/toolbar";
17
+ import { settingsField, updateSettingsEffect, resolveThemeDark, InitialSettingsFacet } from "./panels/settings";
18
18
  import { StyleModule } from "style-mod";
19
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();
20
65
  export const CodeblockFacet = Facet.define({
21
66
  combine: (values) => values[0]
22
67
  });
@@ -27,11 +72,16 @@ export const languageServerCompartment = new Compartment();
27
72
  export const indentationCompartment = new Compartment();
28
73
  export const readOnlyCompartment = new Compartment();
29
74
  export const lineWrappingCompartment = new Compartment();
75
+ export const terminalCompartment = new Compartment();
76
+ export const lineNumbersCompartment = new Compartment();
77
+ export const foldGutterCompartment = new Compartment();
30
78
  // Effects + Fields for async file handling
31
79
  export const openFileEffect = StateEffect.define();
32
80
  export const fileLoadedEffect = StateEffect.define();
33
81
  // Light mode/dark mode theme toggle
34
82
  export const setThemeEffect = StateEffect.define();
83
+ // SVG preview toggle
84
+ export const toggleSvgPreviewEffect = StateEffect.define();
35
85
  // Holds the current file lifecycle
36
86
  export const currentFileField = StateField.define({
37
87
  create(state) {
@@ -97,23 +147,36 @@ export const renderMarkdownCode = (code, parser, highlighter) => {
97
147
  return result.getHTML();
98
148
  };
99
149
  // Main codeblock factory
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 })),
102
- currentFileField,
103
- languageSupportCompartment.of([]),
104
- languageServerCompartment.of([]),
105
- indentationCompartment.of(indentUnit.of(" ")),
106
- readOnlyCompartment.of(EditorState.readOnly.of(false)),
107
- lineWrappingCompartment.of([]),
108
- tooltips({ position: "fixed" }),
109
- showPanel.of(toolbar ? toolbarPanel : null),
110
- settingsField,
111
- codeblockTheme,
112
- codeblockView,
113
- keymap.of(navigationKeymap.concat([indentWithTab])),
114
- vscodeLightDark,
115
- searchResultsField,
116
- ];
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
+ };
117
180
  // ViewPlugin reacts to field state & effects, with microtask scheduling to avoid nested updates
118
181
  // Inject @font-face for Nerd Font icons (idempotent)
119
182
  let nerdFontInjected = false;
@@ -135,23 +198,184 @@ const codeblockView = ViewPlugin.define((view) => {
135
198
  StyleModule.mount(document, vscodeStyleMod);
136
199
  injectNerdFontFace();
137
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
+ });
138
232
  // Debounced save
139
233
  const save = debounce(async () => {
140
234
  const fileState = view.state.field(currentFileField);
141
235
  if (fileState.path) {
236
+ const content = view.state.doc.toString();
142
237
  // confirm parent exists
143
238
  const parent = dirname(fileState.path);
144
239
  if (parent) {
145
240
  await fs.mkdir(parent, { recursive: true }).catch(console.error);
146
241
  }
147
- await fs.writeFile(fileState.path, view.state.doc.toString()).catch(console.error);
242
+ await fs.writeFile(fileState.path, content).catch(console.error);
148
243
  LSP.notifyFileChanged(fileState.path, FileChangeType.Changed);
244
+ // Notify other views of the same file
245
+ fileChangeBus.notify(fileState.path, content, view);
149
246
  }
150
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
+ }
151
270
  // Guard to prevent duplicate opens for same path while loading
152
271
  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;
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
+ }
155
379
  async function setLanguageSupport(language) {
156
380
  if (!language)
157
381
  return;
@@ -218,7 +442,7 @@ const codeblockView = ViewPlugin.define((view) => {
218
442
  }
219
443
  const unit = detectIndentationUnit(content) || " ";
220
444
  // Lazily pre-fill TypeScript lib definitions when a TS/JS file is first opened
221
- const tsExtensions = ['ts', 'tsx', 'js', 'jsx'];
445
+ const tsExtensions = ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs', 'mts', 'cts'];
222
446
  const { typescript } = view.state.facet(CodeblockFacet);
223
447
  let libFiles;
224
448
  if (typescript?.resolveLib && ext && tsExtensions.includes(ext)) {
@@ -227,16 +451,51 @@ const codeblockView = ViewPlugin.define((view) => {
227
451
  else {
228
452
  libFiles = getCachedLibFiles();
229
453
  }
230
- let lsp = lang ? await LSP.client({ language: lang, path, fs, libFiles }) : null;
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
+ }
231
464
  activePath = path;
232
- safeDispatch(view, {
233
- changes: { from: 0, to: view.state.doc.length, insert: content },
234
- effects: [
235
- indentationCompartment.reconfigure(indentUnit.of(unit)),
236
- fileLoadedEffect.of({ path, content, language: lang }),
237
- languageServerCompartment.reconfigure(lsp ? [lsp] : []),
238
- ]
239
- });
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);
240
499
  }
241
500
  catch (e) {
242
501
  console.error("Failed to open file", e);
@@ -257,12 +516,25 @@ const codeblockView = ViewPlugin.define((view) => {
257
516
  update(u) {
258
517
  // React to explicit openFileEffect requests
259
518
  for (let e of u.transactions.flatMap(t => t.effects)) {
260
- 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
+ }
261
528
  queueMicrotask(() => handleOpen(e.value.path));
529
+ }
262
530
  if (e.is(setThemeEffect)) {
263
531
  const dark = e.value.dark;
264
532
  u.view.dom.setAttribute('data-theme', dark ? 'dark' : 'light');
265
533
  }
534
+ if (e.is(toggleSvgPreviewEffect)) {
535
+ const newMode = svgViewMode === 'preview' ? 'source' : 'preview';
536
+ showSvgView(view.state.doc.toString(), newMode);
537
+ }
266
538
  }
267
539
  // Keep read-only in sync with loading state without dispatching new transactions
268
540
  const prev = u.startState.field(currentFileField);
@@ -271,21 +543,46 @@ const codeblockView = ViewPlugin.define((view) => {
271
543
  // Reconfigure readOnly via compartment inside the same update when possible
272
544
  safeDispatch(view, { effects: readOnlyCompartment.reconfigure(EditorState.readOnly.of(next.loading)) });
273
545
  }
274
- if (u.docChanged && u.state.field(settingsField).autosave)
546
+ if (u.docChanged && !receivingExternalUpdate && u.state.field(settingsField).autosave)
275
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
+ }
276
567
  // If fs changed via facet reconfig, refresh handle references
277
568
  const newFs = u.state.facet(CodeblockFacet).fs;
278
569
  if (fs !== newFs)
279
570
  fs = newFs;
571
+ },
572
+ destroy() {
573
+ if (unsubscribeFileChanges) {
574
+ unsubscribeFileChanges();
575
+ unsubscribeFileChanges = null;
576
+ }
577
+ unsubscribeSettings();
578
+ removePreview();
579
+ save.cancel();
280
580
  }
281
581
  };
282
582
  });
283
583
  export const basicSetup = (() => [
284
- lineNumbers(),
285
- highlightActiveLineGutter(),
286
584
  highlightSpecialChars(),
287
585
  history(),
288
- foldGutter(),
289
586
  drawSelection(),
290
587
  dropCursor(),
291
588
  EditorState.allowMultipleSelections.of(true),
@@ -307,10 +604,18 @@ export const basicSetup = (() => [
307
604
  ...lintKeymap
308
605
  ])
309
606
  ])();
310
- export function createCodeblock({ parent, fs, filepath, language, content = '', cwd = '/', toolbar = true, index, dark, typescript }) {
607
+ export function createCodeblock({ parent, fs, filepath, language, content = '', cwd = '/', toolbar = true, index, dark, settings, typescript }) {
311
608
  const state = EditorState.create({
312
609
  doc: content,
313
- extensions: [basicSetup, codeblock({ content, fs, filepath, cwd, language, toolbar, index, dark, typescript })]
610
+ extensions: [basicSetup, codeblock({ content, fs, filepath, cwd, language, toolbar, index, dark, settings, typescript })]
314
611
  });
315
- return new EditorView({ state, parent });
612
+ const view = new EditorView({ state, parent });
613
+ return view;
316
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,5 +1,6 @@
1
- export { createCodeblock, codeblock, basicSetup, type CodeblockConfig, CodeblockFacet, setThemeEffect } from "./editor";
2
- export { settingsField, updateSettingsEffect, type EditorSettings } from "./panels/footer";
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";
3
4
  export { LspLog, type LspLogEntry } from "./utils/lsp";
4
5
  export { Vfs as CodeblockFS } from './utils/fs';
5
6
  export * from './utils/snapshot';
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
- export { createCodeblock, codeblock, basicSetup, CodeblockFacet, setThemeEffect } from "./editor";
2
- export { settingsField, updateSettingsEffect } from "./panels/footer";
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";
3
4
  export { LspLog } from "./utils/lsp";
4
5
  export { Vfs as CodeblockFS } from './utils/fs';
5
6
  export * from './utils/snapshot';
@@ -43,6 +43,10 @@ export declare const extOrLanguageToLanguageId: {
43
43
  readonly ts: "javascript";
44
44
  readonly jsx: "javascript";
45
45
  readonly tsx: "javascript";
46
+ readonly mjs: "javascript";
47
+ readonly cjs: "javascript";
48
+ readonly mts: "javascript";
49
+ readonly cts: "javascript";
46
50
  readonly python: "python";
47
51
  readonly py: "python";
48
52
  readonly ruby: "ruby";
@@ -80,6 +84,7 @@ export declare const extOrLanguageToLanguageId: {
80
84
  readonly yaml: "yaml";
81
85
  readonly yml: "yaml";
82
86
  readonly xml: "xml";
87
+ readonly svg: "xml";
83
88
  readonly markdown: "markdown";
84
89
  readonly md: "markdown";
85
90
  readonly toml: "toml";
@@ -30,8 +30,10 @@ const languageSupportMap = {
30
30
  return less();
31
31
  },
32
32
  json: async () => {
33
- const { json } = await import('@codemirror/lang-json');
34
- return json();
33
+ // Use JavaScript mode for JSON files — it handles both strict JSON
34
+ // and lenient JSON-like syntax (unquoted keys, trailing commas, comments)
35
+ const { javascript } = await import('@codemirror/lang-javascript');
36
+ return javascript();
35
37
  },
36
38
  xml: async () => {
37
39
  const { xml } = await import('@codemirror/lang-xml');
@@ -147,6 +149,10 @@ export const extOrLanguageToLanguageId = {
147
149
  ts: 'javascript',
148
150
  jsx: 'javascript',
149
151
  tsx: 'javascript',
152
+ mjs: 'javascript',
153
+ cjs: 'javascript',
154
+ mts: 'javascript',
155
+ cts: 'javascript',
150
156
  python: 'python',
151
157
  py: 'python',
152
158
  ruby: 'ruby',
@@ -184,6 +190,7 @@ export const extOrLanguageToLanguageId = {
184
190
  yaml: 'yaml',
185
191
  yml: 'yaml',
186
192
  xml: 'xml',
193
+ svg: 'xml',
187
194
  markdown: 'markdown',
188
195
  md: 'markdown',
189
196
  toml: 'toml',
@@ -1,5 +1,5 @@
1
1
  import { EditorView } from "@codemirror/view";
2
- import { StateField } from "@codemirror/state";
2
+ import { Facet, StateField } from "@codemirror/state";
3
3
  export interface EditorSettings {
4
4
  theme: 'light' | 'dark' | 'system';
5
5
  fontSize: number;
@@ -9,7 +9,13 @@ export interface EditorSettings {
9
9
  lspLogEnabled: boolean;
10
10
  agentUrl: string;
11
11
  terminalEnabled: boolean;
12
+ maxVisibleLines: number;
13
+ showLineNumbers: boolean;
14
+ showFoldGutter: boolean;
15
+ autoHideToolbar: boolean;
12
16
  }
17
+ /** Facet carrying initial settings so settingsField.create() can read them without circular imports. */
18
+ export declare const InitialSettingsFacet: Facet<Partial<EditorSettings>, Partial<EditorSettings>>;
13
19
  export declare const updateSettingsEffect: import("@codemirror/state").StateEffectType<Partial<EditorSettings>>;
14
20
  export declare const settingsField: StateField<EditorSettings>;
15
21
  export declare function resolveThemeDark(theme: EditorSettings['theme']): boolean;