@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.
- package/dist/editor.d.ts +30 -3
- package/dist/editor.js +416 -43
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/lsps/index.d.ts +5 -0
- package/dist/lsps/index.js +9 -2
- package/dist/lsps/typescript.d.ts +3 -1
- package/dist/lsps/typescript.js +8 -17
- package/dist/panels/settings.d.ts +22 -0
- package/dist/panels/settings.js +267 -0
- package/dist/panels/terminal.d.ts +3 -0
- package/dist/panels/terminal.js +76 -0
- package/dist/panels/toolbar.d.ts +53 -3
- package/dist/panels/toolbar.js +1336 -164
- package/dist/panels/toolbar.test.js +20 -14
- package/dist/rpc/transport.d.ts +2 -11
- package/dist/rpc/transport.js +19 -35
- package/dist/themes/index.js +226 -13
- package/dist/themes/vscode.js +3 -2
- package/dist/types.d.ts +5 -0
- package/dist/utils/fs.d.ts +22 -3
- package/dist/utils/fs.js +126 -21
- package/dist/utils/lsp.d.ts +26 -15
- package/dist/utils/lsp.js +79 -44
- package/dist/utils/search.d.ts +2 -0
- package/dist/utils/search.js +13 -4
- package/dist/utils/typescript-defaults.d.ts +57 -0
- package/dist/utils/typescript-defaults.js +208 -0
- package/dist/utils/typescript-defaults.test.d.ts +1 -0
- package/dist/utils/typescript-defaults.test.js +197 -0
- package/dist/workers/fs.worker.d.ts +4 -8
- package/dist/workers/fs.worker.js +30 -60
- package/dist/workers/javascript.worker.js +11 -9
- package/package.json +8 -4
- 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-BwEqZcql.ts +0 -109
- package/dist/assets/go-CTD25R5P.js +0 -1
- package/dist/assets/haskell-BWDZoCOh.js +0 -1
- package/dist/assets/index-9HdhmM_Y.js +0 -1
- package/dist/assets/index-C-QhPFHP.js +0 -3
- package/dist/assets/index-C3BnE2cG.js +0 -222
- package/dist/assets/index-CGx5MZO7.js +0 -6
- package/dist/assets/index-CIuq3uTk.js +0 -1
- package/dist/assets/index-CXFONXS8.js +0 -1
- package/dist/assets/index-D5Z27j1C.js +0 -1
- package/dist/assets/index-DWOBdRjn.js +0 -1
- package/dist/assets/index-Dvu-FFzd.js +0 -1
- package/dist/assets/index-Dx_VuNNd.js +0 -1
- package/dist/assets/index-I0dlv-r3.js +0 -1
- package/dist/assets/index-MGle_v2x.js +0 -1
- package/dist/assets/index-N-GE7HTU.js +0 -1
- package/dist/assets/index-aEsF5o-7.js +0 -2
- package/dist/assets/index-as7ELo0J.js +0 -1
- package/dist/assets/index-gUUzXNuP.js +0 -1
- package/dist/assets/index-pGm0qkrJ.js +0 -13
- package/dist/assets/javascript.worker-C1zGArKk.js +0 -527
- 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/example.spec.d.ts +0 -5
- package/dist/e2e/example.spec.js +0 -44
- package/dist/index.html +0 -16
- package/dist/resources/config.json +0 -13
- package/dist/snapshot.bin +0 -0
- 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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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,
|
|
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)
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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';
|