@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.
- package/dist/editor.d.ts +23 -2
- package/dist/editor.js +346 -41
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/lsps/index.d.ts +5 -0
- package/dist/lsps/index.js +9 -2
- package/dist/panels/{footer.d.ts → settings.d.ts} +7 -1
- package/dist/panels/{footer.js → settings.js} +12 -3
- package/dist/panels/terminal.d.ts +3 -0
- package/dist/panels/terminal.js +76 -0
- package/dist/panels/toolbar.d.ts +40 -3
- package/dist/panels/toolbar.js +919 -160
- package/dist/themes/index.js +63 -17
- package/dist/types.d.ts +5 -0
- package/dist/utils/fs.d.ts +7 -0
- package/dist/utils/fs.js +41 -15
- package/dist/workers/fs.worker.d.ts +4 -8
- package/dist/workers/fs.worker.js +27 -53
- package/package.json +6 -3
- package/dist/assets/clike-C8IJ2oj_.js +0 -1
- package/dist/assets/cmake-BQqOBYOt.js +0 -1
- package/dist/assets/dockerfile-C_y-rIpk.js +0 -1
- package/dist/assets/fs.worker-DfanUHpQ.js +0 -21
- package/dist/assets/go-CTD25R5P.js +0 -1
- package/dist/assets/haskell-BWDZoCOh.js +0 -1
- package/dist/assets/index-BAnLzvMk.js +0 -1
- package/dist/assets/index-BBC9WDX6.js +0 -1
- package/dist/assets/index-BEXYxRro.js +0 -1
- package/dist/assets/index-BfYmUKH9.js +0 -13
- package/dist/assets/index-BhaTNAWE.js +0 -1
- package/dist/assets/index-CCbYDSng.js +0 -1
- package/dist/assets/index-CIi8tLT6.js +0 -1
- package/dist/assets/index-CaANcgI2.js +0 -3
- package/dist/assets/index-CkWzFNzm.js +0 -208
- package/dist/assets/index-D_XGv9QZ.js +0 -1
- package/dist/assets/index-DkmiPfkD.js +0 -1
- package/dist/assets/index-DmNlLMQ4.js +0 -6
- package/dist/assets/index-DmX_vI7D.js +0 -1
- package/dist/assets/index-DogEEevD.js +0 -1
- package/dist/assets/index-DsDl5qZV.js +0 -2
- package/dist/assets/index-gAy5mDg-.js +0 -1
- package/dist/assets/index-i5qJLB2h.js +0 -1
- package/dist/assets/javascript.worker-ClsyHOLi.js +0 -552
- package/dist/assets/lua-BgMRiT3U.js +0 -1
- package/dist/assets/perl-CdXCOZ3F.js +0 -1
- package/dist/assets/process-Dw9K5EnD.js +0 -1357
- package/dist/assets/properties-C78fOPTZ.js +0 -1
- package/dist/assets/ruby-B2Rjki9n.js +0 -1
- package/dist/assets/shell-CjFT_Tl9.js +0 -1
- package/dist/assets/swift-BzpIVaGY.js +0 -1
- package/dist/assets/toml-BXUEaScT.js +0 -1
- package/dist/assets/vb-CmGdzxic.js +0 -1
- package/dist/e2e/editor.spec.d.ts +0 -1
- package/dist/e2e/editor.spec.js +0 -309
- package/dist/e2e/example.spec.d.ts +0 -5
- package/dist/e2e/example.spec.js +0 -44
- package/dist/index.html +0 -15
- package/dist/resources/config.json +0 -13
- 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<
|
|
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/
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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';
|
package/dist/lsps/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/lsps/index.js
CHANGED
|
@@ -30,8 +30,10 @@ const languageSupportMap = {
|
|
|
30
30
|
return less();
|
|
31
31
|
},
|
|
32
32
|
json: async () => {
|
|
33
|
-
|
|
34
|
-
|
|
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;
|