wgsl-edit 0.0.25 → 0.0.26
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/README.md +49 -9
- package/dist/SaveEndpoint.d.mts +5 -0
- package/dist/SaveEndpoint.mjs +5 -0
- package/dist/SaveMiddleware.d.mts +9 -0
- package/dist/SaveMiddleware.mjs +49 -0
- package/dist/{WgslEdit-ByXfb3R9.js → WgslEdit-CNK80480.js} +252 -118
- package/dist/WgslEdit.d.ts +45 -1
- package/dist/WgslEdit.js +1 -1
- package/dist/autosave.d.mts +11 -0
- package/dist/autosave.mjs +18 -0
- package/dist/index.js +1 -1
- package/dist/jsx-preact.d.ts +12 -0
- package/dist/jsx-preact.js +0 -0
- package/dist/wgsl-edit.js +291 -501
- package/package.json +24 -6
- package/src/SaveEndpoint.ts +2 -0
- package/src/SaveMiddleware.ts +71 -0
- package/src/WgslEdit.ts +288 -157
- package/src/autosave.ts +24 -0
- package/src/jsx-preact.ts +15 -0
- package/src/test/Autosave.e2e.ts +100 -0
- package/src/test/E2eUtil.ts +6 -0
- package/src/test/Undo.e2e.ts +91 -0
- package/src/test/WgslEdit.e2e.ts +1 -4
|
@@ -5,10 +5,14 @@ import { fileToModulePath, link } from "wesl";
|
|
|
5
5
|
import { autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap } from "@codemirror/autocomplete";
|
|
6
6
|
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
|
7
7
|
import { searchKeymap } from "@codemirror/search";
|
|
8
|
-
import { Compartment, EditorState, Text } from "@codemirror/state";
|
|
8
|
+
import { Compartment, EditorState, Text, Transaction } from "@codemirror/state";
|
|
9
9
|
import { EditorView, crosshairCursor, drawSelection, dropCursor, gutters, highlightActiveLine, highlightActiveLineGutter, highlightSpecialChars, keymap, lineNumbers, rectangularSelection } from "@codemirror/view";
|
|
10
10
|
import { tags } from "@lezer/highlight";
|
|
11
11
|
import { fetchPackagesByName } from "wesl-fetch";
|
|
12
|
+
//#region src/SaveEndpoint.ts
|
|
13
|
+
/** Shared between client (WgslEdit) and server (SaveMiddleware) so they can't drift. */
|
|
14
|
+
const saveEndpoint = "/__wgsl-edit/save";
|
|
15
|
+
//#endregion
|
|
12
16
|
//#region src/WgslEdit.css?inline
|
|
13
17
|
var WgslEdit_default = ":host {\n --tab-bar-bg: transparent;\n --tab-border: #ccc;\n --tab-color: #999;\n --tab-active-bg: #fff;\n --tab-active-color: #222;\n --tab-accent: #5f61d8;\n\n display: flex;\n flex-direction: column;\n position: relative;\n min-height: 100px;\n}\n\n:host(.dark) {\n --tab-bar-bg: transparent;\n --tab-border: #555;\n --tab-color: #999;\n --tab-active-bg: #1e1e1e;\n --tab-active-color: #fff;\n}\n\n.tab-bar {\n --bar-v: 6px;\n --bar-h: 8px;\n display: flex;\n align-items: center;\n gap: 14px;\n padding: var(--bar-v) var(--bar-h);\n background: var(--tab-bar-bg);\n flex-shrink: 0;\n position: relative;\n}\n\n.tab-bar::after {\n content: \"\";\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 1px;\n background: var(--tab-border);\n}\n\n.tab {\n --tab-v: 5px;\n --tab-h: 12px;\n display: flex;\n align-items: center;\n gap: 16px;\n padding: var(--tab-v) var(--tab-h);\n background: transparent;\n border: none;\n border-radius: 0;\n color: var(--tab-color);\n cursor: pointer;\n position: relative;\n font-size: 15px;\n font-family: system-ui, sans-serif;\n}\n\n.tab.active {\n color: var(--tab-accent);\n font-weight: 600;\n padding-bottom: calc(var(--tab-v) + var(--bar-v) + 0.5px);\n margin-bottom: calc(-1 * (var(--bar-v) + 0.5px));\n position: relative;\n z-index: 1;\n border-bottom: 2px solid var(--tab-accent);\n}\n\n.tab.active:first-child {\n margin-left: calc(-1 * var(--bar-h));\n padding-left: calc(var(--tab-h) + var(--bar-h));\n}\n\n.tab-close {\n position: absolute;\n right: -8px;\n /* top: 2px; */\n width: 16px;\n height: 16px;\n padding: 0;\n border: none;\n background: transparent;\n color: inherit;\n cursor: pointer;\n opacity: 0;\n font-size: 18px;\n line-height: 1;\n}\n\n.tab:hover .tab-close {\n opacity: 0.6;\n}\n\n.tab-close:hover {\n opacity: 1;\n}\n\n.tab-rename {\n background: var(--tab-active-bg);\n border: 1px solid var(--tab-border);\n border-radius: 4px;\n color: var(--tab-active-color);\n font-size: 15px;\n font-family: system-ui, sans-serif;\n padding: 0 4px;\n outline: none;\n}\n\n.tab-add {\n padding: 0 10px;\n background: transparent;\n border: none;\n color: var(--tab-color);\n cursor: pointer;\n font-size: 25px;\n line-height: 1;\n}\n\n.tab-add:hover {\n color: var(--tab-active-color);\n}\n\n.editor-container {\n flex: 1;\n min-height: 0;\n width: 100%;\n padding: var(--editor-padding, 16px 0 0);\n}\n\n.cm-editor {\n height: 100%;\n font-size: var(--editor-font-size, 14px);\n}\n\n.cm-scroller {\n overflow: auto;\n}\n\n.snackbar {\n position: absolute;\n bottom: 16px;\n left: 16px;\n background: #e8e8e8;\n color: #333;\n padding: 10px 20px;\n border-radius: 8px;\n font-size: 14px;\n font-family: system-ui, sans-serif;\n line-height: 1.4;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.2s;\n z-index: 10;\n}\n\n:host(.dark) .snackbar {\n background: #3a3a3a;\n color: #e0e0e0;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n}\n\n.snackbar.visible {\n opacity: 1;\n}\n";
|
|
14
18
|
//#endregion
|
|
@@ -47,7 +51,8 @@ var WgslEdit = class extends HTMLElement {
|
|
|
47
51
|
"lint-from",
|
|
48
52
|
"line-numbers",
|
|
49
53
|
"fetch-libs",
|
|
50
|
-
"gpu-lint"
|
|
54
|
+
"gpu-lint",
|
|
55
|
+
"autosave"
|
|
51
56
|
];
|
|
52
57
|
editorView = null;
|
|
53
58
|
editorContainer;
|
|
@@ -76,6 +81,10 @@ var WgslEdit = class extends HTMLElement {
|
|
|
76
81
|
_fetchingPkgs = /* @__PURE__ */ new Set();
|
|
77
82
|
_fetchedPkgs = /* @__PURE__ */ new Set();
|
|
78
83
|
_snackTimer;
|
|
84
|
+
_devSaveListener = null;
|
|
85
|
+
_saveTimer;
|
|
86
|
+
_dirtyFiles = /* @__PURE__ */ new Set();
|
|
87
|
+
_switchingFile = false;
|
|
79
88
|
_externalDiagnostics = [];
|
|
80
89
|
_lintFromEl = null;
|
|
81
90
|
/** Bound listeners for lint-from element's compile events. */
|
|
@@ -99,10 +108,13 @@ var WgslEdit = class extends HTMLElement {
|
|
|
99
108
|
connectedCallback() {
|
|
100
109
|
this.initEditor();
|
|
101
110
|
this.loadInitialContent();
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
111
|
+
for (const p of [
|
|
112
|
+
"conditions",
|
|
113
|
+
"source",
|
|
114
|
+
"sources",
|
|
115
|
+
"project",
|
|
116
|
+
"autosave"
|
|
117
|
+
]) upgradeProperty(this, p);
|
|
106
118
|
}
|
|
107
119
|
disconnectedCallback() {
|
|
108
120
|
this.connectLintSource(null);
|
|
@@ -110,25 +122,43 @@ var WgslEdit = class extends HTMLElement {
|
|
|
110
122
|
this.editorView = null;
|
|
111
123
|
}
|
|
112
124
|
attributeChangedCallback(name, _old, value) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
125
|
+
switch (name) {
|
|
126
|
+
case "src":
|
|
127
|
+
if (value && this.editorView) this.loadFromUrl(value);
|
|
128
|
+
break;
|
|
129
|
+
case "readonly":
|
|
130
|
+
this.updateReadonly();
|
|
131
|
+
break;
|
|
132
|
+
case "theme":
|
|
133
|
+
this.theme = value || "auto";
|
|
134
|
+
break;
|
|
135
|
+
case "tabs":
|
|
136
|
+
this._tabs = value !== "false";
|
|
137
|
+
this.renderTabs();
|
|
138
|
+
break;
|
|
139
|
+
case "lint":
|
|
140
|
+
this._lint = value || "on";
|
|
141
|
+
this.updateLint();
|
|
142
|
+
break;
|
|
143
|
+
case "lint-from":
|
|
144
|
+
this.connectLintSource(value);
|
|
145
|
+
break;
|
|
146
|
+
case "line-numbers":
|
|
147
|
+
this._lineNumbers = value === "true";
|
|
148
|
+
this.updateLineNumbers();
|
|
149
|
+
break;
|
|
150
|
+
case "fetch-libs":
|
|
151
|
+
this._fetchLibs = value !== "false";
|
|
152
|
+
this.updateLint();
|
|
153
|
+
break;
|
|
154
|
+
case "gpu-lint":
|
|
155
|
+
this._gpuLint = value !== "off";
|
|
156
|
+
this.updateLint();
|
|
157
|
+
break;
|
|
158
|
+
case "autosave":
|
|
159
|
+
if (value !== null && value !== "false") this.enableDevSave();
|
|
160
|
+
else this.disableDevSave();
|
|
161
|
+
break;
|
|
132
162
|
}
|
|
133
163
|
}
|
|
134
164
|
/** Conditions for conditional compilation (@if/@elif/@else). */
|
|
@@ -140,6 +170,10 @@ var WgslEdit = class extends HTMLElement {
|
|
|
140
170
|
this.updateLint();
|
|
141
171
|
this.dispatchChange();
|
|
142
172
|
}
|
|
173
|
+
/** Reconfigure a compartment via the active editor view (no-op if unmounted). */
|
|
174
|
+
reconfigure(comp, ext) {
|
|
175
|
+
this.editorView?.dispatch({ effects: comp.reconfigure(ext) });
|
|
176
|
+
}
|
|
143
177
|
/** Active file content (single-file API). */
|
|
144
178
|
get source() {
|
|
145
179
|
return this.editorView?.state.doc.toString() ?? this._pendingSource ?? "";
|
|
@@ -147,27 +181,30 @@ var WgslEdit = class extends HTMLElement {
|
|
|
147
181
|
/** Set active file content (single-file API). Auto-creates a default file entry. */
|
|
148
182
|
set source(value) {
|
|
149
183
|
if (!this._activeFile && this._files.size === 0) {
|
|
150
|
-
this._files.set("main.wesl", { doc:
|
|
184
|
+
this._files.set("main.wesl", { doc: toDoc(value) });
|
|
151
185
|
this._activeFile = "main.wesl";
|
|
152
186
|
this.renderTabs();
|
|
153
187
|
}
|
|
154
188
|
if (this.editorView) {
|
|
155
|
-
const
|
|
156
|
-
this.editorView.dispatch({
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
189
|
+
const docLength = this.editorView.state.doc.length;
|
|
190
|
+
this.editorView.dispatch({
|
|
191
|
+
changes: {
|
|
192
|
+
from: 0,
|
|
193
|
+
to: docLength,
|
|
194
|
+
insert: value
|
|
195
|
+
},
|
|
196
|
+
annotations: Transaction.addToHistory.of(false)
|
|
197
|
+
});
|
|
161
198
|
} else {
|
|
162
199
|
this._pendingSource = value;
|
|
163
200
|
const entry = this._files.get(this._activeFile);
|
|
164
|
-
if (entry) entry.doc =
|
|
201
|
+
if (entry) entry.doc = toDoc(value);
|
|
165
202
|
}
|
|
166
203
|
}
|
|
167
204
|
/** All file contents keyed by module path (e.g., "package::main"). */
|
|
168
205
|
get sources() {
|
|
169
206
|
this.saveCurrentFileState();
|
|
170
|
-
const pkg = this.
|
|
207
|
+
const pkg = this.pkgName();
|
|
171
208
|
const result = {};
|
|
172
209
|
for (const [tabName, state] of this._files) result[fileToModulePath(tabName, pkg, false)] = state.doc.toString();
|
|
173
210
|
return result;
|
|
@@ -177,12 +214,13 @@ var WgslEdit = class extends HTMLElement {
|
|
|
177
214
|
this._files.clear();
|
|
178
215
|
for (const [key, content] of Object.entries(value)) {
|
|
179
216
|
const tabName = toTabName(key);
|
|
180
|
-
this._files.set(tabName, { doc:
|
|
217
|
+
this._files.set(tabName, { doc: toDoc(content) });
|
|
181
218
|
}
|
|
182
219
|
const firstKey = Object.keys(value)[0];
|
|
183
220
|
if (firstKey) this.switchToFile(toTabName(firstKey));
|
|
184
221
|
this.renderTabs();
|
|
185
222
|
}
|
|
223
|
+
/** Snapshot of all editor state needed to link: sources, conditions, libs, root module. */
|
|
186
224
|
get project() {
|
|
187
225
|
return {
|
|
188
226
|
weslSrc: this.sources,
|
|
@@ -190,20 +228,22 @@ var WgslEdit = class extends HTMLElement {
|
|
|
190
228
|
conditions: this._conditions,
|
|
191
229
|
constants: this._constants,
|
|
192
230
|
libs: this._libs,
|
|
193
|
-
packageName: this._packageName
|
|
231
|
+
packageName: this._packageName,
|
|
232
|
+
shaderRoot: this.shaderRoot ?? void 0
|
|
194
233
|
};
|
|
195
234
|
}
|
|
196
235
|
set project(value) {
|
|
197
236
|
const { weslSrc, rootModuleName, conditions } = value;
|
|
198
|
-
const { constants, packageName, libs } = value;
|
|
237
|
+
const { constants, packageName, libs, shaderRoot } = value;
|
|
199
238
|
if (conditions !== void 0) this._conditions = conditions;
|
|
200
239
|
if (constants !== void 0) this._constants = constants;
|
|
201
240
|
if (packageName !== void 0) this._packageName = packageName;
|
|
202
241
|
if (libs !== void 0) this._libs = libs;
|
|
203
242
|
if (rootModuleName !== void 0) this._rootModuleName = rootModuleName;
|
|
243
|
+
if (shaderRoot !== void 0 && !this.hasAttribute("shader-root")) this.shaderRoot = shaderRoot;
|
|
204
244
|
if (weslSrc) {
|
|
205
245
|
this.sources = weslSrc;
|
|
206
|
-
const tab = toTabName(
|
|
246
|
+
const tab = toTabName(this._rootModuleName ?? "");
|
|
207
247
|
if (tab) this.activeFile = tab;
|
|
208
248
|
}
|
|
209
249
|
this.updateLint();
|
|
@@ -216,14 +256,13 @@ var WgslEdit = class extends HTMLElement {
|
|
|
216
256
|
})).dest;
|
|
217
257
|
}
|
|
218
258
|
linkParams() {
|
|
219
|
-
const pkg = this._packageName ?? "package";
|
|
220
259
|
return {
|
|
221
260
|
weslSrc: this.sources,
|
|
222
|
-
rootModuleName: this._rootModuleName ??
|
|
261
|
+
rootModuleName: this._rootModuleName ?? this.activeModulePath(),
|
|
223
262
|
conditions: this._conditions,
|
|
224
263
|
constants: this._constants,
|
|
225
264
|
libs: this._libs,
|
|
226
|
-
packageName:
|
|
265
|
+
packageName: this.pkgName()
|
|
227
266
|
};
|
|
228
267
|
}
|
|
229
268
|
get libs() {
|
|
@@ -279,6 +318,20 @@ var WgslEdit = class extends HTMLElement {
|
|
|
279
318
|
if (!value) this.setAttribute("gpu-lint", "off");
|
|
280
319
|
else this.removeAttribute("gpu-lint");
|
|
281
320
|
}
|
|
321
|
+
/** Persist edits to disk via the dev-server save endpoint.
|
|
322
|
+
* Requires `wgslEditAutosave()` to be installed in vite.config.ts. */
|
|
323
|
+
get autosave() {
|
|
324
|
+
return this._devSaveListener !== null;
|
|
325
|
+
}
|
|
326
|
+
set autosave(value) {
|
|
327
|
+
if (value) {
|
|
328
|
+
this.enableDevSave();
|
|
329
|
+
this.setAttribute("autosave", "");
|
|
330
|
+
} else {
|
|
331
|
+
this.disableDevSave();
|
|
332
|
+
this.removeAttribute("autosave");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
282
335
|
/** Whether to auto-fetch missing library packages from npm (default: true). */
|
|
283
336
|
get fetchLibs() {
|
|
284
337
|
return this._fetchLibs;
|
|
@@ -328,13 +381,15 @@ var WgslEdit = class extends HTMLElement {
|
|
|
328
381
|
if (value) this.setAttribute("shader-root", value);
|
|
329
382
|
else this.removeAttribute("shader-root");
|
|
330
383
|
}
|
|
384
|
+
/** Add a new file and switch to it. No-op if `name` already exists. */
|
|
331
385
|
addFile(name, content = "") {
|
|
332
386
|
if (this._files.has(name)) return;
|
|
333
|
-
this._files.set(name, { doc:
|
|
387
|
+
this._files.set(name, { doc: toDoc(content) });
|
|
334
388
|
this.switchToFile(name);
|
|
335
389
|
this.renderTabs();
|
|
336
390
|
this.dispatchFileChange("add", name);
|
|
337
391
|
}
|
|
392
|
+
/** Remove a file. No-op if it is missing or is the last remaining file. */
|
|
338
393
|
removeFile(name) {
|
|
339
394
|
if (!this._files.has(name) || this._files.size <= 1) return;
|
|
340
395
|
this._files.delete(name);
|
|
@@ -345,6 +400,7 @@ var WgslEdit = class extends HTMLElement {
|
|
|
345
400
|
this.renderTabs();
|
|
346
401
|
this.dispatchFileChange("remove", name);
|
|
347
402
|
}
|
|
403
|
+
/** Rename a file, preserving its document and editor state. No-op on collision. */
|
|
348
404
|
renameFile(oldName, newName) {
|
|
349
405
|
const state = this._files.get(oldName);
|
|
350
406
|
if (!state || this._files.has(newName)) return;
|
|
@@ -360,28 +416,36 @@ var WgslEdit = class extends HTMLElement {
|
|
|
360
416
|
this.saveCurrentFileState();
|
|
361
417
|
this._activeFile = name;
|
|
362
418
|
const fileState = this._files.get(name);
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const effects = EditorView.scrollIntoView(fileState.scrollPos ?? 0);
|
|
370
|
-
this.editorView.dispatch({
|
|
371
|
-
changes,
|
|
372
|
-
selection: fileState.selection,
|
|
373
|
-
effects
|
|
374
|
-
});
|
|
419
|
+
const view = this.editorView;
|
|
420
|
+
if (view) {
|
|
421
|
+
const state = fileState.state ?? this.createFileState(fileState.doc);
|
|
422
|
+
fileState.state = state;
|
|
423
|
+
view.setState(state);
|
|
424
|
+
if (fileState.scrollPos != null) view.scrollDOM.scrollTop = fileState.scrollPos;
|
|
375
425
|
}
|
|
376
426
|
this.renderTabs();
|
|
377
427
|
}
|
|
428
|
+
/** Build a fresh EditorState seeded with `doc` and the current extension set. */
|
|
429
|
+
createFileState(doc) {
|
|
430
|
+
return EditorState.create({
|
|
431
|
+
doc,
|
|
432
|
+
extensions: this.buildExtensions()
|
|
433
|
+
});
|
|
434
|
+
}
|
|
378
435
|
saveCurrentFileState() {
|
|
379
436
|
const view = this.editorView;
|
|
380
|
-
const
|
|
381
|
-
if (!view || !
|
|
382
|
-
state
|
|
383
|
-
|
|
384
|
-
|
|
437
|
+
const fileState = this._activeFile ? this._files.get(this._activeFile) : void 0;
|
|
438
|
+
if (!view || !fileState) return;
|
|
439
|
+
fileState.state = view.state;
|
|
440
|
+
fileState.doc = view.state.doc;
|
|
441
|
+
fileState.selection = view.state.selection;
|
|
442
|
+
fileState.scrollPos = view.scrollDOM.scrollTop;
|
|
443
|
+
}
|
|
444
|
+
pkgName() {
|
|
445
|
+
return this._packageName ?? "package";
|
|
446
|
+
}
|
|
447
|
+
activeModulePath() {
|
|
448
|
+
return fileToModulePath(this._activeFile, this.pkgName(), false);
|
|
385
449
|
}
|
|
386
450
|
dispatchChange() {
|
|
387
451
|
this.dispatchEvent(new CustomEvent("change", { detail: this.project }));
|
|
@@ -393,25 +457,86 @@ var WgslEdit = class extends HTMLElement {
|
|
|
393
457
|
};
|
|
394
458
|
this.dispatchEvent(new CustomEvent("file-change", { detail }));
|
|
395
459
|
}
|
|
460
|
+
scheduleAutosave() {
|
|
461
|
+
this._dirtyFiles.add(this._activeFile);
|
|
462
|
+
clearTimeout(this._saveTimer);
|
|
463
|
+
this._saveTimer = setTimeout(() => this.fireAutosave(), 500);
|
|
464
|
+
}
|
|
465
|
+
/** Dispatch an `autosave` event with a fresh project snapshot and the dirty-file list. */
|
|
466
|
+
fireAutosave() {
|
|
467
|
+
const dirty = [...this._dirtyFiles];
|
|
468
|
+
this._dirtyFiles.clear();
|
|
469
|
+
const detail = {
|
|
470
|
+
project: this.project,
|
|
471
|
+
dirty
|
|
472
|
+
};
|
|
473
|
+
this.dispatchEvent(new CustomEvent("autosave", { detail }));
|
|
474
|
+
}
|
|
475
|
+
enableDevSave() {
|
|
476
|
+
if (this._devSaveListener) return;
|
|
477
|
+
this._devSaveListener = (e) => this.devSave(e);
|
|
478
|
+
this.addEventListener("autosave", this._devSaveListener);
|
|
479
|
+
}
|
|
480
|
+
disableDevSave() {
|
|
481
|
+
if (!this._devSaveListener) return;
|
|
482
|
+
this.removeEventListener("autosave", this._devSaveListener);
|
|
483
|
+
this._devSaveListener = null;
|
|
484
|
+
}
|
|
485
|
+
/** Built-in `autosave` listener: POST each dirty file to the dev-server save endpoint. */
|
|
486
|
+
async devSave(e) {
|
|
487
|
+
const root = this.shaderRoot;
|
|
488
|
+
if (!root) return;
|
|
489
|
+
const { project, dirty } = e.detail;
|
|
490
|
+
const weslSrc = project.weslSrc;
|
|
491
|
+
if (!weslSrc) return;
|
|
492
|
+
const pkg = this.pkgName();
|
|
493
|
+
for (const file of dirty) {
|
|
494
|
+
const content = weslSrc[fileToModulePath(file, pkg, false)];
|
|
495
|
+
if (content === void 0) continue;
|
|
496
|
+
try {
|
|
497
|
+
const res = await fetch(saveEndpoint, {
|
|
498
|
+
method: "POST",
|
|
499
|
+
headers: { "Content-Type": "application/json" },
|
|
500
|
+
body: JSON.stringify({
|
|
501
|
+
root,
|
|
502
|
+
file,
|
|
503
|
+
content
|
|
504
|
+
})
|
|
505
|
+
});
|
|
506
|
+
if (!res.ok) return this.devSaveFailed(`HTTP ${res.status}`);
|
|
507
|
+
} catch (err) {
|
|
508
|
+
return this.devSaveFailed(err instanceof Error ? err.message : String(err));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
/** One-shot disable on first failure: avoids spamming the console on every keystroke. */
|
|
513
|
+
devSaveFailed(reason) {
|
|
514
|
+
this.disableDevSave();
|
|
515
|
+
console.error(`wgsl-edit: autosave disabled (${reason}). Make sure wgslEditAutosave() is installed in vite.config.ts.`);
|
|
516
|
+
}
|
|
517
|
+
/** First-time setup: read attributes, parse inline content, mount the EditorView. */
|
|
396
518
|
initEditor() {
|
|
397
519
|
this.readInitialAttributes();
|
|
398
520
|
this.parseInlineContent();
|
|
399
521
|
this._mediaQuery = matchMedia("(prefers-color-scheme: dark)");
|
|
400
522
|
this._mediaQuery.addEventListener("change", () => this.updateTheme());
|
|
401
523
|
const firstFile = this._files.keys().next().value;
|
|
402
|
-
const
|
|
524
|
+
const firstDoc = firstFile && this._files.get(firstFile).doc.toString();
|
|
525
|
+
const initialDoc = this._pendingSource ?? firstDoc ?? "";
|
|
403
526
|
this._pendingSource = null;
|
|
404
527
|
if (firstFile) this._activeFile = firstFile;
|
|
405
528
|
else if (initialDoc) {
|
|
406
|
-
this._files.set("main.wesl", { doc:
|
|
529
|
+
this._files.set("main.wesl", { doc: toDoc(initialDoc) });
|
|
407
530
|
this._activeFile = "main.wesl";
|
|
408
531
|
}
|
|
409
|
-
const
|
|
532
|
+
const state = this.createFileState(initialDoc);
|
|
533
|
+
const active = this._activeFile ? this._files.get(this._activeFile) : null;
|
|
534
|
+
if (active) {
|
|
535
|
+
active.state = state;
|
|
536
|
+
active.doc = state.doc;
|
|
537
|
+
}
|
|
410
538
|
this.editorView = new EditorView({
|
|
411
|
-
state
|
|
412
|
-
doc: initialDoc,
|
|
413
|
-
extensions
|
|
414
|
-
}),
|
|
539
|
+
state,
|
|
415
540
|
parent: this.editorContainer
|
|
416
541
|
});
|
|
417
542
|
this.renderTabs();
|
|
@@ -428,6 +553,7 @@ var WgslEdit = class extends HTMLElement {
|
|
|
428
553
|
const lintFromAttr = this.getAttribute("lint-from");
|
|
429
554
|
if (lintFromAttr) this.connectLintSource(lintFromAttr);
|
|
430
555
|
}
|
|
556
|
+
/** Assemble the full CodeMirror extension set for a fresh EditorState. */
|
|
431
557
|
buildExtensions() {
|
|
432
558
|
const baseTheme = EditorView.theme({
|
|
433
559
|
".cm-content": { padding: "0" },
|
|
@@ -472,6 +598,7 @@ var WgslEdit = class extends HTMLElement {
|
|
|
472
598
|
this._externalDiagnostics = [];
|
|
473
599
|
this.saveCurrentFileState();
|
|
474
600
|
this.dispatchChange();
|
|
601
|
+
if (!this._switchingFile) this.scheduleAutosave();
|
|
475
602
|
}
|
|
476
603
|
})
|
|
477
604
|
];
|
|
@@ -482,11 +609,11 @@ var WgslEdit = class extends HTMLElement {
|
|
|
482
609
|
return [EditorView.theme({}, { dark: isDark }), isDark ? darkColors : lightColors];
|
|
483
610
|
}
|
|
484
611
|
updateTheme() {
|
|
485
|
-
this.
|
|
612
|
+
this.reconfigure(this.themeCompartment, this.resolveTheme());
|
|
486
613
|
}
|
|
487
614
|
updateReadonly() {
|
|
488
615
|
const ext = EditorState.readOnly.of(this.readonly);
|
|
489
|
-
this.
|
|
616
|
+
this.reconfigure(this.readonlyCompartment, ext);
|
|
490
617
|
this.renderTabs();
|
|
491
618
|
}
|
|
492
619
|
resolveLint() {
|
|
@@ -494,7 +621,7 @@ var WgslEdit = class extends HTMLElement {
|
|
|
494
621
|
const useGpuLint = this._gpuLint && !this._lintFromEl;
|
|
495
622
|
return createWeslLinter({
|
|
496
623
|
getSources: () => this.sources,
|
|
497
|
-
rootModule: () =>
|
|
624
|
+
rootModule: () => this.activeModulePath(),
|
|
498
625
|
conditions: () => this._conditions,
|
|
499
626
|
packageName: () => this._packageName,
|
|
500
627
|
getExternalDiagnostics: () => this._externalDiagnostics,
|
|
@@ -507,11 +634,8 @@ var WgslEdit = class extends HTMLElement {
|
|
|
507
634
|
/** Link WESL->WGSL and validate via WebGPU, returning CodeMirror diagnostics. */
|
|
508
635
|
async gpuValidate() {
|
|
509
636
|
const { validateWgsl } = await import("./GpuValidator-BZSULsmE.js");
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
const messages = await validateWgsl(linked.dest);
|
|
513
|
-
const pkg = params.packageName ?? "package";
|
|
514
|
-
return mapGpuDiagnostics(messages, linked, this._activeFile, pkg);
|
|
637
|
+
const linked = await link(this.linkParams());
|
|
638
|
+
return mapGpuDiagnostics(await validateWgsl(linked.dest), linked, this._activeFile, this.pkgName());
|
|
515
639
|
}
|
|
516
640
|
/** Fetch missing library packages, deduplicating in-flight requests. */
|
|
517
641
|
async fetchLibsOnDemand(names) {
|
|
@@ -538,14 +662,15 @@ var WgslEdit = class extends HTMLElement {
|
|
|
538
662
|
}
|
|
539
663
|
}
|
|
540
664
|
updateLint() {
|
|
541
|
-
this.
|
|
665
|
+
this.reconfigure(this.lintCompartment, this.resolveLint());
|
|
542
666
|
}
|
|
543
667
|
/** Listen for compile-error/compile-success events from a lint source element. */
|
|
544
668
|
connectLintSource(id) {
|
|
545
669
|
const hadExternal = !!this._lintFromEl;
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
670
|
+
const prev = this._lintFromEl;
|
|
671
|
+
if (prev) {
|
|
672
|
+
prev.removeEventListener("compile-error", this._boundCompileError);
|
|
673
|
+
prev.removeEventListener("compile-success", this._boundCompileSuccess);
|
|
549
674
|
this._lintFromEl = null;
|
|
550
675
|
}
|
|
551
676
|
this._externalDiagnostics = [];
|
|
@@ -557,23 +682,24 @@ var WgslEdit = class extends HTMLElement {
|
|
|
557
682
|
}
|
|
558
683
|
if (hadExternal !== !!this._lintFromEl) this.updateLint();
|
|
559
684
|
}
|
|
685
|
+
/** Convert external compile-error locations into CodeMirror diagnostics for the active file. */
|
|
560
686
|
onCompileError(e) {
|
|
561
687
|
const detail = e.detail;
|
|
562
688
|
if (!this.editorView || detail.source === "wesl") return;
|
|
563
689
|
const doc = this.editorView.state.doc;
|
|
564
|
-
const pkg = this.
|
|
565
|
-
const activeModule =
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
return fileToModulePath(loc.file, pkg, false) === activeModule;
|
|
569
|
-
}).map((loc) => {
|
|
690
|
+
const pkg = this.pkgName();
|
|
691
|
+
const activeModule = this.activeModulePath();
|
|
692
|
+
const inActiveFile = (loc) => !loc.file || fileToModulePath(loc.file, pkg, false) === activeModule;
|
|
693
|
+
this._externalDiagnostics = detail.locations.filter(inActiveFile).map((loc) => {
|
|
570
694
|
const line = doc.line(Math.max(1, Math.min(loc.line, doc.lines)));
|
|
571
695
|
const from = Math.min(line.from + (loc.column ?? 0), doc.length);
|
|
696
|
+
const to = Math.min(from + (loc.length ?? 1), doc.length);
|
|
697
|
+
const { severity, message } = loc;
|
|
572
698
|
return {
|
|
573
699
|
from,
|
|
574
|
-
to
|
|
575
|
-
severity
|
|
576
|
-
message
|
|
700
|
+
to,
|
|
701
|
+
severity,
|
|
702
|
+
message,
|
|
577
703
|
source: "WebGPU"
|
|
578
704
|
};
|
|
579
705
|
});
|
|
@@ -588,21 +714,20 @@ var WgslEdit = class extends HTMLElement {
|
|
|
588
714
|
return this._lineNumbers ? [] : EditorView.theme({ ".cm-gutters": { display: "none" } });
|
|
589
715
|
}
|
|
590
716
|
updateLineNumbers() {
|
|
591
|
-
|
|
592
|
-
this.editorView?.dispatch({ effects: this.lineNumbersCompartment.reconfigure(ext) });
|
|
717
|
+
this.reconfigure(this.lineNumbersCompartment, this.resolveLineNumbers());
|
|
593
718
|
}
|
|
594
719
|
/** Parse script tags into _files. Supports single or multi-file via data-name. */
|
|
595
720
|
parseInlineContent() {
|
|
596
721
|
const scripts = Array.from(this.querySelectorAll("script[type=\"text/wgsl\"], script[type=\"text/wesl\"]"));
|
|
597
722
|
if (scripts.length === 0) {
|
|
598
723
|
const content = this.textContent?.trim() ?? "";
|
|
599
|
-
if (content) this._files.set("main.wesl", { doc:
|
|
724
|
+
if (content) this._files.set("main.wesl", { doc: toDoc(content) });
|
|
600
725
|
return;
|
|
601
726
|
}
|
|
602
727
|
for (const script of scripts) {
|
|
603
728
|
const name = script.getAttribute("data-name") || "main.wesl";
|
|
604
729
|
const content = script.textContent?.trim() ?? "";
|
|
605
|
-
this._files.set(name, { doc:
|
|
730
|
+
this._files.set(name, { doc: toDoc(content) });
|
|
606
731
|
}
|
|
607
732
|
}
|
|
608
733
|
renderTabs() {
|
|
@@ -647,28 +772,27 @@ var WgslEdit = class extends HTMLElement {
|
|
|
647
772
|
});
|
|
648
773
|
return btn;
|
|
649
774
|
}
|
|
775
|
+
/** Replace the tab name span with an editable input; commit on Enter/blur, cancel on Escape. */
|
|
650
776
|
startRenameTab(tab, nameSpan, oldName) {
|
|
651
777
|
const input = document.createElement("input");
|
|
652
778
|
input.className = "tab-rename";
|
|
653
779
|
input.value = oldName;
|
|
654
780
|
input.size = Math.max(oldName.length, 8);
|
|
781
|
+
const cancelRename = () => {
|
|
782
|
+
nameSpan.style.display = "";
|
|
783
|
+
input.remove();
|
|
784
|
+
};
|
|
655
785
|
const finishRename = () => {
|
|
656
786
|
const newName = input.value.trim() || oldName;
|
|
657
787
|
if (newName !== oldName && !this._files.has(newName)) this.renameFile(oldName, newName);
|
|
658
|
-
else
|
|
659
|
-
nameSpan.style.display = "";
|
|
660
|
-
input.remove();
|
|
661
|
-
}
|
|
788
|
+
else cancelRename();
|
|
662
789
|
};
|
|
663
790
|
input.addEventListener("keydown", (e) => {
|
|
664
791
|
if (e.key === "Enter") {
|
|
665
792
|
e.preventDefault();
|
|
666
793
|
finishRename();
|
|
667
794
|
}
|
|
668
|
-
if (e.key === "Escape")
|
|
669
|
-
nameSpan.style.display = "";
|
|
670
|
-
input.remove();
|
|
671
|
-
}
|
|
795
|
+
if (e.key === "Escape") cancelRename();
|
|
672
796
|
});
|
|
673
797
|
input.addEventListener("blur", finishRename);
|
|
674
798
|
input.addEventListener("input", () => {
|
|
@@ -696,6 +820,7 @@ var WgslEdit = class extends HTMLElement {
|
|
|
696
820
|
if (src) this.loadFromUrl(src);
|
|
697
821
|
}
|
|
698
822
|
};
|
|
823
|
+
/** Build a CodeMirror highlight style from a WESL color palette. */
|
|
699
824
|
function weslColors(c) {
|
|
700
825
|
return syntaxHighlighting(HighlightStyle.define([
|
|
701
826
|
{
|
|
@@ -763,24 +888,7 @@ function weslColors(c) {
|
|
|
763
888
|
fontStyle: "normal"
|
|
764
889
|
} }));
|
|
765
890
|
}
|
|
766
|
-
/**
|
|
767
|
-
function mapGpuDiagnostics(messages, linked, activeFile, pkg) {
|
|
768
|
-
const { sourceMap } = linked;
|
|
769
|
-
const active = fileToModulePath(activeFile, pkg, false);
|
|
770
|
-
return messages.flatMap((msg) => {
|
|
771
|
-
const srcPos = sourceMap.destToSrc(msg.offset);
|
|
772
|
-
if ((srcPos.src.path ? fileToModulePath(srcPos.src.path, pkg, false) : null) !== active) return [];
|
|
773
|
-
const endPos = sourceMap.destToSrc(msg.offset + msg.length);
|
|
774
|
-
const from = srcPos.position;
|
|
775
|
-
return {
|
|
776
|
-
from,
|
|
777
|
-
to: endPos.position > from ? endPos.position : from + 1,
|
|
778
|
-
severity: msg.severity,
|
|
779
|
-
message: msg.message,
|
|
780
|
-
source: "WebGPU"
|
|
781
|
-
};
|
|
782
|
-
});
|
|
783
|
-
}
|
|
891
|
+
/** Lazily build and cache the shared component stylesheet. */
|
|
784
892
|
function getStyles() {
|
|
785
893
|
if (!cachedStyleSheet) {
|
|
786
894
|
cachedStyleSheet = new CSSStyleSheet();
|
|
@@ -788,7 +896,8 @@ function getStyles() {
|
|
|
788
896
|
}
|
|
789
897
|
return cachedStyleSheet;
|
|
790
898
|
}
|
|
791
|
-
/** Absorb instance properties set before custom element upgrade.
|
|
899
|
+
/** Absorb instance properties set before custom element upgrade.
|
|
900
|
+
* Duplicated in WgslPlay.ts. Later, extract to a shared package. */
|
|
792
901
|
function upgradeProperty(el, prop) {
|
|
793
902
|
if (Object.hasOwn(el, prop)) {
|
|
794
903
|
const value = el[prop];
|
|
@@ -796,10 +905,35 @@ function upgradeProperty(el, prop) {
|
|
|
796
905
|
el[prop] = value;
|
|
797
906
|
}
|
|
798
907
|
}
|
|
908
|
+
/** Build a CodeMirror Text doc from a string. */
|
|
909
|
+
function toDoc(s) {
|
|
910
|
+
return Text.of(s.split("\n"));
|
|
911
|
+
}
|
|
799
912
|
/** Convert a module path or file path to a tab name: "package::main" -> "main", "main.wesl" -> "main.wesl" */
|
|
800
913
|
function toTabName(key) {
|
|
801
914
|
if (key.includes("::")) return key.replace(/^[^:]+::/, "").replaceAll("::", "/");
|
|
802
915
|
return key.replace(/^\.\//, "");
|
|
803
916
|
}
|
|
917
|
+
/** Map GPU validation messages back to source positions via the source map. */
|
|
918
|
+
function mapGpuDiagnostics(messages, linked, activeFile, pkg) {
|
|
919
|
+
const { sourceMap } = linked;
|
|
920
|
+
const active = fileToModulePath(activeFile, pkg, false);
|
|
921
|
+
return messages.flatMap((msg) => {
|
|
922
|
+
const srcPos = sourceMap.destToSrc(msg.offset);
|
|
923
|
+
const path = srcPos.src.path;
|
|
924
|
+
if ((path ? fileToModulePath(path, pkg, false) : null) !== active) return [];
|
|
925
|
+
const endPos = sourceMap.destToSrc(msg.offset + msg.length);
|
|
926
|
+
const from = srcPos.position;
|
|
927
|
+
const to = endPos.position > from ? endPos.position : from + 1;
|
|
928
|
+
const { severity, message } = msg;
|
|
929
|
+
return {
|
|
930
|
+
from,
|
|
931
|
+
to,
|
|
932
|
+
severity,
|
|
933
|
+
message,
|
|
934
|
+
source: "WebGPU"
|
|
935
|
+
};
|
|
936
|
+
});
|
|
937
|
+
}
|
|
804
938
|
//#endregion
|
|
805
939
|
export { WgslEdit as t };
|