wgsl-edit 0.0.21 → 0.0.23
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/Language.js +1 -3
- package/dist/WgslEdit-CYb8MVnU.js +762 -0
- package/dist/WgslEdit.js +3 -762
- package/dist/index.js +2 -4
- package/dist/wgsl-edit.js +19 -180
- package/package.json +5 -5
- package/dist/WgslEdit-BiLVs-hz.js +0 -5
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
import { createWeslLinter, wesl } from "./Language.js";
|
|
2
|
+
import { HighlightStyle, bracketMatching, defaultHighlightStyle, foldGutter, foldKeymap, indentOnInput, syntaxHighlighting } from "@codemirror/language";
|
|
3
|
+
import { forceLinting, lintKeymap } from "@codemirror/lint";
|
|
4
|
+
import { fileToModulePath, link } from "wesl";
|
|
5
|
+
import { autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap } from "@codemirror/autocomplete";
|
|
6
|
+
import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
|
|
7
|
+
import { searchKeymap } from "@codemirror/search";
|
|
8
|
+
import { Compartment, EditorState, Text } from "@codemirror/state";
|
|
9
|
+
import { EditorView, crosshairCursor, drawSelection, dropCursor, gutters, highlightActiveLine, highlightActiveLineGutter, highlightSpecialChars, keymap, lineNumbers, rectangularSelection } from "@codemirror/view";
|
|
10
|
+
import { tags } from "@lezer/highlight";
|
|
11
|
+
import { fetchPackagesByName } from "wesl-fetch";
|
|
12
|
+
//#region src/WgslEdit.css?inline
|
|
13
|
+
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
|
+
//#endregion
|
|
15
|
+
//#region src/WgslEdit.ts
|
|
16
|
+
const light = {
|
|
17
|
+
variable: "#000f80",
|
|
18
|
+
keyword: "#0000ff",
|
|
19
|
+
control: "#af00db",
|
|
20
|
+
type: "#891a1a",
|
|
21
|
+
fn: "#795e26",
|
|
22
|
+
number: "#098658",
|
|
23
|
+
comment: "#008000",
|
|
24
|
+
modulePath: "#0070c1"
|
|
25
|
+
};
|
|
26
|
+
const dark = {
|
|
27
|
+
variable: "#9cdcfe",
|
|
28
|
+
keyword: "#569cd6",
|
|
29
|
+
control: "#c586c0",
|
|
30
|
+
type: "#4ec9b0",
|
|
31
|
+
fn: "#dcdcaa",
|
|
32
|
+
number: "#b5cea8",
|
|
33
|
+
comment: "#6a9955",
|
|
34
|
+
modulePath: "#4fc1ff"
|
|
35
|
+
};
|
|
36
|
+
const lightColors = weslColors(light);
|
|
37
|
+
const darkColors = weslColors(dark);
|
|
38
|
+
let cachedStyleSheet;
|
|
39
|
+
var WgslEdit = class extends HTMLElement {
|
|
40
|
+
static observedAttributes = [
|
|
41
|
+
"src",
|
|
42
|
+
"readonly",
|
|
43
|
+
"theme",
|
|
44
|
+
"shader-root",
|
|
45
|
+
"tabs",
|
|
46
|
+
"lint",
|
|
47
|
+
"lint-from",
|
|
48
|
+
"line-numbers",
|
|
49
|
+
"fetch-libs"
|
|
50
|
+
];
|
|
51
|
+
editorView = null;
|
|
52
|
+
editorContainer;
|
|
53
|
+
tabBar;
|
|
54
|
+
snackbar;
|
|
55
|
+
readonlyCompartment = new Compartment();
|
|
56
|
+
themeCompartment = new Compartment();
|
|
57
|
+
lintCompartment = new Compartment();
|
|
58
|
+
lineNumbersCompartment = new Compartment();
|
|
59
|
+
_pendingSource = null;
|
|
60
|
+
_theme = "auto";
|
|
61
|
+
_mediaQuery = null;
|
|
62
|
+
_lineNumbers = false;
|
|
63
|
+
_files = /* @__PURE__ */ new Map();
|
|
64
|
+
_activeFile = "";
|
|
65
|
+
_rootModuleName;
|
|
66
|
+
_tabs = true;
|
|
67
|
+
_lint = "on";
|
|
68
|
+
_fetchLibs = true;
|
|
69
|
+
_conditions = {};
|
|
70
|
+
_packageName;
|
|
71
|
+
_libs = [];
|
|
72
|
+
_ignorePackages = ["constants", "env"];
|
|
73
|
+
_fetchingPkgs = /* @__PURE__ */ new Set();
|
|
74
|
+
_fetchedPkgs = /* @__PURE__ */ new Set();
|
|
75
|
+
_snackTimer;
|
|
76
|
+
_externalDiagnostics = [];
|
|
77
|
+
_lintFromEl = null;
|
|
78
|
+
/** Bound listeners for lint-from element's compile events. */
|
|
79
|
+
_boundCompileError = this.onCompileError.bind(this);
|
|
80
|
+
_boundCompileSuccess = this.onCompileSuccess.bind(this);
|
|
81
|
+
constructor() {
|
|
82
|
+
super();
|
|
83
|
+
const shadow = this.attachShadow({ mode: "open" });
|
|
84
|
+
shadow.adoptedStyleSheets = [getStyles()];
|
|
85
|
+
this.tabBar = document.createElement("div");
|
|
86
|
+
this.tabBar.className = "tab-bar";
|
|
87
|
+
shadow.appendChild(this.tabBar);
|
|
88
|
+
this.editorContainer = document.createElement("div");
|
|
89
|
+
this.editorContainer.className = "editor-container";
|
|
90
|
+
shadow.appendChild(this.editorContainer);
|
|
91
|
+
this.snackbar = document.createElement("div");
|
|
92
|
+
this.snackbar.className = "snackbar";
|
|
93
|
+
this.snackbar.textContent = "Loading…";
|
|
94
|
+
shadow.appendChild(this.snackbar);
|
|
95
|
+
}
|
|
96
|
+
connectedCallback() {
|
|
97
|
+
this.initEditor();
|
|
98
|
+
this.loadInitialContent();
|
|
99
|
+
upgradeProperty(this, "conditions");
|
|
100
|
+
upgradeProperty(this, "source");
|
|
101
|
+
upgradeProperty(this, "sources");
|
|
102
|
+
upgradeProperty(this, "project");
|
|
103
|
+
}
|
|
104
|
+
disconnectedCallback() {
|
|
105
|
+
this.connectLintSource(null);
|
|
106
|
+
this.editorView?.destroy();
|
|
107
|
+
this.editorView = null;
|
|
108
|
+
}
|
|
109
|
+
attributeChangedCallback(name, _old, value) {
|
|
110
|
+
if (name === "src" && value && this.editorView) this.loadFromUrl(value);
|
|
111
|
+
else if (name === "readonly") this.updateReadonly();
|
|
112
|
+
else if (name === "theme") this.theme = value || "auto";
|
|
113
|
+
else if (name === "tabs") {
|
|
114
|
+
this._tabs = value !== "false";
|
|
115
|
+
this.renderTabs();
|
|
116
|
+
} else if (name === "lint") {
|
|
117
|
+
this._lint = value || "on";
|
|
118
|
+
this.updateLint();
|
|
119
|
+
} else if (name === "lint-from") this.connectLintSource(value);
|
|
120
|
+
else if (name === "line-numbers") {
|
|
121
|
+
this._lineNumbers = value === "true";
|
|
122
|
+
this.updateLineNumbers();
|
|
123
|
+
} else if (name === "fetch-libs") {
|
|
124
|
+
this._fetchLibs = value !== "false";
|
|
125
|
+
this.updateLint();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** Conditions for conditional compilation (@if/@elif/@else). */
|
|
129
|
+
get conditions() {
|
|
130
|
+
return this._conditions;
|
|
131
|
+
}
|
|
132
|
+
set conditions(value) {
|
|
133
|
+
this._conditions = value;
|
|
134
|
+
this.updateLint();
|
|
135
|
+
this.dispatchChange();
|
|
136
|
+
}
|
|
137
|
+
/** Active file content (single-file API). */
|
|
138
|
+
get source() {
|
|
139
|
+
return this.editorView?.state.doc.toString() ?? this._pendingSource ?? "";
|
|
140
|
+
}
|
|
141
|
+
/** Set active file content (single-file API). Auto-creates a default file entry. */
|
|
142
|
+
set source(value) {
|
|
143
|
+
if (!this._activeFile && this._files.size === 0) {
|
|
144
|
+
this._files.set("main.wesl", { doc: Text.of(value.split("\n")) });
|
|
145
|
+
this._activeFile = "main.wesl";
|
|
146
|
+
this.renderTabs();
|
|
147
|
+
}
|
|
148
|
+
if (this.editorView) {
|
|
149
|
+
const to = this.editorView.state.doc.length;
|
|
150
|
+
this.editorView.dispatch({ changes: {
|
|
151
|
+
from: 0,
|
|
152
|
+
to,
|
|
153
|
+
insert: value
|
|
154
|
+
} });
|
|
155
|
+
} else {
|
|
156
|
+
this._pendingSource = value;
|
|
157
|
+
const entry = this._files.get(this._activeFile);
|
|
158
|
+
if (entry) entry.doc = Text.of(value.split("\n"));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** All file contents keyed by module path (e.g., "package::main"). */
|
|
162
|
+
get sources() {
|
|
163
|
+
this.saveCurrentFileState();
|
|
164
|
+
const pkg = this._packageName ?? "package";
|
|
165
|
+
const result = {};
|
|
166
|
+
for (const [tabName, state] of this._files) result[fileToModulePath(tabName, pkg, false)] = state.doc.toString();
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
/** Set all files (replaces existing). */
|
|
170
|
+
set sources(value) {
|
|
171
|
+
this._files.clear();
|
|
172
|
+
for (const [key, content] of Object.entries(value)) {
|
|
173
|
+
const tabName = toTabName(key);
|
|
174
|
+
this._files.set(tabName, { doc: Text.of(content.split("\n")) });
|
|
175
|
+
}
|
|
176
|
+
const firstKey = Object.keys(value)[0];
|
|
177
|
+
if (firstKey) this.switchToFile(toTabName(firstKey));
|
|
178
|
+
this.renderTabs();
|
|
179
|
+
}
|
|
180
|
+
/** Load a full project config (sources, conditions, packageName, etc.). */
|
|
181
|
+
set project(value) {
|
|
182
|
+
const { weslSrc, rootModuleName, conditions, packageName, libs } = value;
|
|
183
|
+
if (conditions !== void 0) this._conditions = conditions;
|
|
184
|
+
if (packageName !== void 0) this._packageName = packageName;
|
|
185
|
+
if (libs !== void 0) this._libs = libs;
|
|
186
|
+
if (rootModuleName !== void 0) this._rootModuleName = rootModuleName;
|
|
187
|
+
if (weslSrc) {
|
|
188
|
+
this.sources = weslSrc;
|
|
189
|
+
const tab = toTabName(rootModuleName ?? this._rootModuleName ?? "");
|
|
190
|
+
if (tab) this.activeFile = tab;
|
|
191
|
+
}
|
|
192
|
+
this.updateLint();
|
|
193
|
+
}
|
|
194
|
+
/** Link/compile WESL sources into WGSL. Returns the compiled WGSL string. */
|
|
195
|
+
async link(options) {
|
|
196
|
+
const pkg = this._packageName ?? "package";
|
|
197
|
+
const rootModuleName = this._rootModuleName ?? fileToModulePath(this._activeFile, pkg, false);
|
|
198
|
+
return (await link({
|
|
199
|
+
weslSrc: this.sources,
|
|
200
|
+
rootModuleName,
|
|
201
|
+
conditions: this._conditions,
|
|
202
|
+
libs: this._libs,
|
|
203
|
+
packageName: pkg,
|
|
204
|
+
...options
|
|
205
|
+
})).dest;
|
|
206
|
+
}
|
|
207
|
+
/** Library bundles for linking (set via project). */
|
|
208
|
+
get libs() {
|
|
209
|
+
return this._libs;
|
|
210
|
+
}
|
|
211
|
+
/** Root module for linking (stable across tab switches). */
|
|
212
|
+
get rootModuleName() {
|
|
213
|
+
return this._rootModuleName;
|
|
214
|
+
}
|
|
215
|
+
set rootModuleName(value) {
|
|
216
|
+
this._rootModuleName = value;
|
|
217
|
+
this.dispatchChange();
|
|
218
|
+
}
|
|
219
|
+
/** Currently active file name (selected tab). */
|
|
220
|
+
get activeFile() {
|
|
221
|
+
return this._activeFile;
|
|
222
|
+
}
|
|
223
|
+
/** Switch to a file by name. */
|
|
224
|
+
set activeFile(name) {
|
|
225
|
+
this.switchToFile(name);
|
|
226
|
+
}
|
|
227
|
+
/** List of file names in order. */
|
|
228
|
+
get fileNames() {
|
|
229
|
+
return Array.from(this._files.keys());
|
|
230
|
+
}
|
|
231
|
+
/** Tab bar visibility. */
|
|
232
|
+
get tabs() {
|
|
233
|
+
return this._tabs;
|
|
234
|
+
}
|
|
235
|
+
set tabs(value) {
|
|
236
|
+
this._tabs = value;
|
|
237
|
+
this.renderTabs();
|
|
238
|
+
}
|
|
239
|
+
/** Lint mode: "on" (default) or "off". */
|
|
240
|
+
get lint() {
|
|
241
|
+
return this._lint;
|
|
242
|
+
}
|
|
243
|
+
set lint(value) {
|
|
244
|
+
this._lint = value;
|
|
245
|
+
this.updateLint();
|
|
246
|
+
}
|
|
247
|
+
/** Line numbers visibility (default: true). */
|
|
248
|
+
get lineNumbers() {
|
|
249
|
+
return this._lineNumbers;
|
|
250
|
+
}
|
|
251
|
+
set lineNumbers(value) {
|
|
252
|
+
this._lineNumbers = value;
|
|
253
|
+
if (value) this.setAttribute("line-numbers", "true");
|
|
254
|
+
else this.removeAttribute("line-numbers");
|
|
255
|
+
}
|
|
256
|
+
/** Whether to auto-fetch missing library packages from npm (default: true). */
|
|
257
|
+
get fetchLibs() {
|
|
258
|
+
return this._fetchLibs;
|
|
259
|
+
}
|
|
260
|
+
set fetchLibs(value) {
|
|
261
|
+
this._fetchLibs = value;
|
|
262
|
+
if (value) this.removeAttribute("fetch-libs");
|
|
263
|
+
else this.setAttribute("fetch-libs", "false");
|
|
264
|
+
}
|
|
265
|
+
/** Whether the editor is currently loading content. */
|
|
266
|
+
get loading() {
|
|
267
|
+
return this.snackbar.classList.contains("visible");
|
|
268
|
+
}
|
|
269
|
+
set loading(value) {
|
|
270
|
+
if (value) this.showSnack("Loading…");
|
|
271
|
+
else this.hideSnack();
|
|
272
|
+
}
|
|
273
|
+
/** Show the snackbar with a message. Auto-hides after `ms` if provided. */
|
|
274
|
+
showSnack(msg, ms) {
|
|
275
|
+
clearTimeout(this._snackTimer);
|
|
276
|
+
this.snackbar.textContent = msg;
|
|
277
|
+
this.snackbar.classList.add("visible");
|
|
278
|
+
console.log("wgsl-edit showSnack:", msg);
|
|
279
|
+
if (ms) this._snackTimer = setTimeout(() => this.hideSnack(), ms);
|
|
280
|
+
}
|
|
281
|
+
hideSnack() {
|
|
282
|
+
clearTimeout(this._snackTimer);
|
|
283
|
+
this.snackbar.classList.remove("visible");
|
|
284
|
+
}
|
|
285
|
+
get readonly() {
|
|
286
|
+
return this.hasAttribute("readonly");
|
|
287
|
+
}
|
|
288
|
+
set readonly(value) {
|
|
289
|
+
if (value) this.setAttribute("readonly", "");
|
|
290
|
+
else this.removeAttribute("readonly");
|
|
291
|
+
}
|
|
292
|
+
get theme() {
|
|
293
|
+
return this._theme;
|
|
294
|
+
}
|
|
295
|
+
set theme(value) {
|
|
296
|
+
this._theme = value;
|
|
297
|
+
this.updateTheme();
|
|
298
|
+
}
|
|
299
|
+
get shaderRoot() {
|
|
300
|
+
return this.getAttribute("shader-root");
|
|
301
|
+
}
|
|
302
|
+
set shaderRoot(value) {
|
|
303
|
+
if (value) this.setAttribute("shader-root", value);
|
|
304
|
+
else this.removeAttribute("shader-root");
|
|
305
|
+
}
|
|
306
|
+
/** Add a new file. */
|
|
307
|
+
addFile(name, content = "") {
|
|
308
|
+
if (this._files.has(name)) return;
|
|
309
|
+
this._files.set(name, { doc: Text.of(content.split("\n")) });
|
|
310
|
+
this.switchToFile(name);
|
|
311
|
+
this.renderTabs();
|
|
312
|
+
this.dispatchFileChange("add", name);
|
|
313
|
+
}
|
|
314
|
+
/** Remove a file. */
|
|
315
|
+
removeFile(name) {
|
|
316
|
+
if (!this._files.has(name) || this._files.size <= 1) return;
|
|
317
|
+
this._files.delete(name);
|
|
318
|
+
if (this._activeFile === name) {
|
|
319
|
+
const firstFile = this._files.keys().next().value;
|
|
320
|
+
this.switchToFile(firstFile);
|
|
321
|
+
}
|
|
322
|
+
this.renderTabs();
|
|
323
|
+
this.dispatchFileChange("remove", name);
|
|
324
|
+
}
|
|
325
|
+
/** Rename a file. */
|
|
326
|
+
renameFile(oldName, newName) {
|
|
327
|
+
const state = this._files.get(oldName);
|
|
328
|
+
if (!state || this._files.has(newName)) return;
|
|
329
|
+
this._files.delete(oldName);
|
|
330
|
+
this._files.set(newName, state);
|
|
331
|
+
if (this._activeFile === oldName) this._activeFile = newName;
|
|
332
|
+
this.renderTabs();
|
|
333
|
+
this.dispatchFileChange("rename", newName);
|
|
334
|
+
}
|
|
335
|
+
/** Switch to a file, saving current state and restoring target state. */
|
|
336
|
+
switchToFile(name) {
|
|
337
|
+
if (!this._files.has(name) || name === this._activeFile) return;
|
|
338
|
+
this.saveCurrentFileState();
|
|
339
|
+
this._activeFile = name;
|
|
340
|
+
const fileState = this._files.get(name);
|
|
341
|
+
if (this.editorView) {
|
|
342
|
+
const changes = {
|
|
343
|
+
from: 0,
|
|
344
|
+
to: this.editorView.state.doc.length,
|
|
345
|
+
insert: fileState.doc.toString()
|
|
346
|
+
};
|
|
347
|
+
const effects = EditorView.scrollIntoView(fileState.scrollPos ?? 0);
|
|
348
|
+
this.editorView.dispatch({
|
|
349
|
+
changes,
|
|
350
|
+
selection: fileState.selection,
|
|
351
|
+
effects
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
this.renderTabs();
|
|
355
|
+
}
|
|
356
|
+
/** Save current editor state to the active file. */
|
|
357
|
+
saveCurrentFileState() {
|
|
358
|
+
if (!this.editorView || !this._activeFile) return;
|
|
359
|
+
const state = this._files.get(this._activeFile);
|
|
360
|
+
if (state) {
|
|
361
|
+
state.doc = this.editorView.state.doc;
|
|
362
|
+
state.selection = this.editorView.state.selection;
|
|
363
|
+
state.scrollPos = this.editorView.scrollDOM.scrollTop;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
dispatchChange() {
|
|
367
|
+
const { source, sources, conditions, _rootModuleName: rootModuleName, libs } = this;
|
|
368
|
+
const detail = {
|
|
369
|
+
source,
|
|
370
|
+
sources,
|
|
371
|
+
rootModuleName,
|
|
372
|
+
conditions,
|
|
373
|
+
libs
|
|
374
|
+
};
|
|
375
|
+
this.dispatchEvent(new CustomEvent("change", { detail }));
|
|
376
|
+
}
|
|
377
|
+
dispatchFileChange(action, file) {
|
|
378
|
+
this.dispatchEvent(new CustomEvent("file-change", { detail: {
|
|
379
|
+
action,
|
|
380
|
+
file
|
|
381
|
+
} }));
|
|
382
|
+
}
|
|
383
|
+
initEditor() {
|
|
384
|
+
this.readInitialAttributes();
|
|
385
|
+
this.parseInlineContent();
|
|
386
|
+
this._mediaQuery = matchMedia("(prefers-color-scheme: dark)");
|
|
387
|
+
this._mediaQuery.addEventListener("change", () => this.updateTheme());
|
|
388
|
+
const firstFile = this._files.keys().next().value;
|
|
389
|
+
const initialDoc = this._pendingSource ?? (firstFile ? this._files.get(firstFile).doc.toString() : "");
|
|
390
|
+
this._pendingSource = null;
|
|
391
|
+
if (firstFile) this._activeFile = firstFile;
|
|
392
|
+
else if (initialDoc) {
|
|
393
|
+
this._files.set("main.wesl", { doc: Text.of(initialDoc.split("\n")) });
|
|
394
|
+
this._activeFile = "main.wesl";
|
|
395
|
+
}
|
|
396
|
+
this.editorView = new EditorView({
|
|
397
|
+
state: EditorState.create({
|
|
398
|
+
doc: initialDoc,
|
|
399
|
+
extensions: this.buildExtensions()
|
|
400
|
+
}),
|
|
401
|
+
parent: this.editorContainer
|
|
402
|
+
});
|
|
403
|
+
this.renderTabs();
|
|
404
|
+
}
|
|
405
|
+
readInitialAttributes() {
|
|
406
|
+
const themeAttr = this.getAttribute("theme");
|
|
407
|
+
if (themeAttr) this._theme = themeAttr;
|
|
408
|
+
const tabsAttr = this.getAttribute("tabs");
|
|
409
|
+
if (tabsAttr !== null) this._tabs = tabsAttr !== "false";
|
|
410
|
+
const lintAttr = this.getAttribute("lint");
|
|
411
|
+
if (lintAttr) this._lint = lintAttr;
|
|
412
|
+
const lineNumAttr = this.getAttribute("line-numbers");
|
|
413
|
+
if (lineNumAttr !== null) this._lineNumbers = lineNumAttr === "true";
|
|
414
|
+
const lintFromAttr = this.getAttribute("lint-from");
|
|
415
|
+
if (lintFromAttr) this.connectLintSource(lintFromAttr);
|
|
416
|
+
}
|
|
417
|
+
buildExtensions() {
|
|
418
|
+
const baseTheme = EditorView.theme({
|
|
419
|
+
".cm-content": { padding: "0" },
|
|
420
|
+
".cm-line": { padding: "0" },
|
|
421
|
+
".cm-panels": { position: "relative" }
|
|
422
|
+
});
|
|
423
|
+
return [
|
|
424
|
+
gutters({ fixed: false }),
|
|
425
|
+
lineNumbers(),
|
|
426
|
+
highlightActiveLineGutter(),
|
|
427
|
+
highlightSpecialChars(),
|
|
428
|
+
history(),
|
|
429
|
+
foldGutter(),
|
|
430
|
+
drawSelection(),
|
|
431
|
+
dropCursor(),
|
|
432
|
+
EditorState.allowMultipleSelections.of(true),
|
|
433
|
+
indentOnInput(),
|
|
434
|
+
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
|
|
435
|
+
bracketMatching(),
|
|
436
|
+
closeBrackets(),
|
|
437
|
+
autocompletion(),
|
|
438
|
+
rectangularSelection(),
|
|
439
|
+
crosshairCursor(),
|
|
440
|
+
highlightActiveLine(),
|
|
441
|
+
keymap.of([
|
|
442
|
+
...closeBracketsKeymap,
|
|
443
|
+
...defaultKeymap,
|
|
444
|
+
...searchKeymap,
|
|
445
|
+
...historyKeymap,
|
|
446
|
+
...foldKeymap,
|
|
447
|
+
...completionKeymap,
|
|
448
|
+
...lintKeymap
|
|
449
|
+
]),
|
|
450
|
+
wesl(),
|
|
451
|
+
baseTheme,
|
|
452
|
+
this.themeCompartment.of(this.resolveTheme()),
|
|
453
|
+
this.readonlyCompartment.of(EditorState.readOnly.of(this.readonly)),
|
|
454
|
+
this.lintCompartment.of(this.resolveLint()),
|
|
455
|
+
this.lineNumbersCompartment.of(this.resolveLineNumbers()),
|
|
456
|
+
EditorView.updateListener.of((update) => {
|
|
457
|
+
if (update.docChanged) {
|
|
458
|
+
this._externalDiagnostics = [];
|
|
459
|
+
this.saveCurrentFileState();
|
|
460
|
+
this.dispatchChange();
|
|
461
|
+
}
|
|
462
|
+
})
|
|
463
|
+
];
|
|
464
|
+
}
|
|
465
|
+
resolveTheme() {
|
|
466
|
+
const isDark = this._theme === "dark" || this._theme === "auto" && matchMedia("(prefers-color-scheme: dark)").matches;
|
|
467
|
+
this.classList.toggle("dark", isDark);
|
|
468
|
+
return [EditorView.theme({}, { dark: isDark }), isDark ? darkColors : lightColors];
|
|
469
|
+
}
|
|
470
|
+
updateTheme() {
|
|
471
|
+
this.editorView?.dispatch({ effects: this.themeCompartment.reconfigure(this.resolveTheme()) });
|
|
472
|
+
}
|
|
473
|
+
updateReadonly() {
|
|
474
|
+
this.editorView?.dispatch({ effects: this.readonlyCompartment.reconfigure(EditorState.readOnly.of(this.readonly)) });
|
|
475
|
+
this.renderTabs();
|
|
476
|
+
}
|
|
477
|
+
resolveLint() {
|
|
478
|
+
if (this._lint === "off") return [];
|
|
479
|
+
return createWeslLinter({
|
|
480
|
+
getSources: () => this.sources,
|
|
481
|
+
rootModule: () => fileToModulePath(this._activeFile, this._packageName ?? "package", false),
|
|
482
|
+
conditions: () => this._conditions,
|
|
483
|
+
packageName: () => this._packageName,
|
|
484
|
+
getExternalDiagnostics: () => this._externalDiagnostics,
|
|
485
|
+
getLibs: () => this._libs,
|
|
486
|
+
fetchLibs: this._fetchLibs ? (pkgs) => this.fetchLibsOnDemand(pkgs) : void 0,
|
|
487
|
+
ignorePackages: () => this._ignorePackages
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
/** Fetch missing library packages, deduplicating in-flight requests. */
|
|
491
|
+
async fetchLibsOnDemand(packageNames) {
|
|
492
|
+
const needed = packageNames.filter((n) => !this._fetchedPkgs.has(n) && !this._fetchingPkgs.has(n));
|
|
493
|
+
if (needed.length === 0) return [];
|
|
494
|
+
for (const n of needed) this._fetchingPkgs.add(n);
|
|
495
|
+
this.showSnack(`Loading ${needed.join(", ")}…`);
|
|
496
|
+
try {
|
|
497
|
+
const bundles = await fetchPackagesByName(needed);
|
|
498
|
+
this._libs = [...this._libs, ...bundles];
|
|
499
|
+
for (const n of needed) this._fetchedPkgs.add(n);
|
|
500
|
+
if (bundles.length > 0) {
|
|
501
|
+
this.showSnack(`Loaded ${needed.join(", ")}`, 3e3);
|
|
502
|
+
this._externalDiagnostics = [];
|
|
503
|
+
this.dispatchChange();
|
|
504
|
+
} else this.hideSnack();
|
|
505
|
+
return bundles;
|
|
506
|
+
} catch (e) {
|
|
507
|
+
console.warn("wgsl-edit: failed to fetch packages:", needed, e);
|
|
508
|
+
this.showSnack(`Failed to load ${needed.join(", ")}`, 3e3);
|
|
509
|
+
return [];
|
|
510
|
+
} finally {
|
|
511
|
+
for (const n of needed) this._fetchingPkgs.delete(n);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
updateLint() {
|
|
515
|
+
this.editorView?.dispatch({ effects: this.lintCompartment.reconfigure(this.resolveLint()) });
|
|
516
|
+
}
|
|
517
|
+
/** Listen for compile-error/compile-success events from a lint source element. */
|
|
518
|
+
connectLintSource(id) {
|
|
519
|
+
if (this._lintFromEl) {
|
|
520
|
+
this._lintFromEl.removeEventListener("compile-error", this._boundCompileError);
|
|
521
|
+
this._lintFromEl.removeEventListener("compile-success", this._boundCompileSuccess);
|
|
522
|
+
this._lintFromEl = null;
|
|
523
|
+
}
|
|
524
|
+
this._externalDiagnostics = [];
|
|
525
|
+
if (!id) return;
|
|
526
|
+
const el = document.getElementById(id);
|
|
527
|
+
if (!el) return;
|
|
528
|
+
this._lintFromEl = el;
|
|
529
|
+
el.addEventListener("compile-error", this._boundCompileError);
|
|
530
|
+
el.addEventListener("compile-success", this._boundCompileSuccess);
|
|
531
|
+
}
|
|
532
|
+
onCompileError(e) {
|
|
533
|
+
const detail = e.detail;
|
|
534
|
+
if (!this.editorView || detail.source === "wesl") return;
|
|
535
|
+
const doc = this.editorView.state.doc;
|
|
536
|
+
const pkg = this._packageName ?? "package";
|
|
537
|
+
const activeModule = fileToModulePath(this._activeFile, pkg, false);
|
|
538
|
+
this._externalDiagnostics = detail.locations.filter((loc) => {
|
|
539
|
+
if (!loc.file) return true;
|
|
540
|
+
return fileToModulePath(loc.file, pkg, false) === activeModule;
|
|
541
|
+
}).map((loc) => {
|
|
542
|
+
const line = doc.line(Math.max(1, Math.min(loc.line, doc.lines)));
|
|
543
|
+
const from = Math.min(line.from + (loc.column ?? 0), doc.length);
|
|
544
|
+
return {
|
|
545
|
+
from,
|
|
546
|
+
to: Math.min(from + (loc.length ?? 1), doc.length),
|
|
547
|
+
severity: loc.severity,
|
|
548
|
+
message: loc.message,
|
|
549
|
+
source: "WebGPU"
|
|
550
|
+
};
|
|
551
|
+
});
|
|
552
|
+
if (this._externalDiagnostics.length) forceLinting(this.editorView);
|
|
553
|
+
}
|
|
554
|
+
onCompileSuccess() {
|
|
555
|
+
if (this._externalDiagnostics.length === 0) return;
|
|
556
|
+
this._externalDiagnostics = [];
|
|
557
|
+
if (this.editorView) forceLinting(this.editorView);
|
|
558
|
+
}
|
|
559
|
+
resolveLineNumbers() {
|
|
560
|
+
return this._lineNumbers ? [] : EditorView.theme({ ".cm-gutters": { display: "none" } });
|
|
561
|
+
}
|
|
562
|
+
updateLineNumbers() {
|
|
563
|
+
this.editorView?.dispatch({ effects: this.lineNumbersCompartment.reconfigure(this.resolveLineNumbers()) });
|
|
564
|
+
}
|
|
565
|
+
/** Parse script tags into _files. Supports single or multi-file via data-name. */
|
|
566
|
+
parseInlineContent() {
|
|
567
|
+
const scripts = Array.from(this.querySelectorAll("script[type=\"text/wgsl\"], script[type=\"text/wesl\"]"));
|
|
568
|
+
if (scripts.length === 0) {
|
|
569
|
+
const content = this.textContent?.trim() ?? "";
|
|
570
|
+
if (content) this._files.set("main.wesl", { doc: Text.of(content.split("\n")) });
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
for (const script of scripts) {
|
|
574
|
+
const name = script.getAttribute("data-name") || "main.wesl";
|
|
575
|
+
const content = script.textContent?.trim() ?? "";
|
|
576
|
+
this._files.set(name, { doc: Text.of(content.split("\n")) });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
/** Render tab bar based on files and visibility mode. */
|
|
580
|
+
renderTabs() {
|
|
581
|
+
this.tabBar.style.display = this._tabs ? "flex" : "none";
|
|
582
|
+
if (!this._tabs) return;
|
|
583
|
+
this.tabBar.innerHTML = "";
|
|
584
|
+
for (const name of this._files.keys()) this.tabBar.appendChild(this.createTab(name));
|
|
585
|
+
if (!this.readonly) this.tabBar.appendChild(this.createAddButton());
|
|
586
|
+
}
|
|
587
|
+
/** Create a tab button for a file. */
|
|
588
|
+
createTab(name) {
|
|
589
|
+
const tab = document.createElement("button");
|
|
590
|
+
tab.className = "tab" + (name === this._activeFile ? " active" : "");
|
|
591
|
+
const nameSpan = document.createElement("span");
|
|
592
|
+
nameSpan.className = "tab-name";
|
|
593
|
+
nameSpan.textContent = name;
|
|
594
|
+
if (!this.readonly) {
|
|
595
|
+
nameSpan.addEventListener("dblclick", (e) => {
|
|
596
|
+
e.stopPropagation();
|
|
597
|
+
this.startRenameTab(tab, nameSpan, name);
|
|
598
|
+
});
|
|
599
|
+
const closeBtn = document.createElement("button");
|
|
600
|
+
closeBtn.className = "tab-close";
|
|
601
|
+
closeBtn.textContent = "×";
|
|
602
|
+
closeBtn.addEventListener("click", (e) => {
|
|
603
|
+
e.stopPropagation();
|
|
604
|
+
this.removeFile(name);
|
|
605
|
+
});
|
|
606
|
+
tab.append(nameSpan, closeBtn);
|
|
607
|
+
} else tab.append(nameSpan);
|
|
608
|
+
tab.addEventListener("click", () => this.switchToFile(name));
|
|
609
|
+
return tab;
|
|
610
|
+
}
|
|
611
|
+
/** Create the "+" button for adding new files. */
|
|
612
|
+
createAddButton() {
|
|
613
|
+
const btn = document.createElement("button");
|
|
614
|
+
btn.className = "tab-add";
|
|
615
|
+
btn.textContent = "+";
|
|
616
|
+
btn.addEventListener("click", () => {
|
|
617
|
+
let name = "new.wesl";
|
|
618
|
+
let i = 1;
|
|
619
|
+
while (this._files.has(name)) name = `new${i++}.wesl`;
|
|
620
|
+
this.addFile(name);
|
|
621
|
+
});
|
|
622
|
+
return btn;
|
|
623
|
+
}
|
|
624
|
+
/** Start inline rename of a tab. */
|
|
625
|
+
startRenameTab(tab, nameSpan, oldName) {
|
|
626
|
+
const input = document.createElement("input");
|
|
627
|
+
input.className = "tab-rename";
|
|
628
|
+
input.value = oldName;
|
|
629
|
+
input.size = Math.max(oldName.length, 8);
|
|
630
|
+
const finishRename = () => {
|
|
631
|
+
const newName = input.value.trim() || oldName;
|
|
632
|
+
if (newName !== oldName && !this._files.has(newName)) this.renameFile(oldName, newName);
|
|
633
|
+
else {
|
|
634
|
+
nameSpan.style.display = "";
|
|
635
|
+
input.remove();
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
input.addEventListener("keydown", (e) => {
|
|
639
|
+
if (e.key === "Enter") {
|
|
640
|
+
e.preventDefault();
|
|
641
|
+
finishRename();
|
|
642
|
+
}
|
|
643
|
+
if (e.key === "Escape") {
|
|
644
|
+
nameSpan.style.display = "";
|
|
645
|
+
input.remove();
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
input.addEventListener("blur", finishRename);
|
|
649
|
+
input.addEventListener("input", () => {
|
|
650
|
+
input.size = Math.max(input.value.length, 8);
|
|
651
|
+
});
|
|
652
|
+
nameSpan.style.display = "none";
|
|
653
|
+
tab.insertBefore(input, nameSpan);
|
|
654
|
+
input.focus();
|
|
655
|
+
input.select();
|
|
656
|
+
}
|
|
657
|
+
async loadFromUrl(url) {
|
|
658
|
+
this.loading = true;
|
|
659
|
+
try {
|
|
660
|
+
const response = await fetch(url);
|
|
661
|
+
if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.status}`);
|
|
662
|
+
this.source = await response.text();
|
|
663
|
+
} catch (e) {
|
|
664
|
+
console.error("wgsl-edit: Failed to load source:", e);
|
|
665
|
+
} finally {
|
|
666
|
+
this.loading = false;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
loadInitialContent() {
|
|
670
|
+
const src = this.getAttribute("src");
|
|
671
|
+
if (src) this.loadFromUrl(src);
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
function weslColors(c) {
|
|
675
|
+
return syntaxHighlighting(HighlightStyle.define([
|
|
676
|
+
{
|
|
677
|
+
tag: tags.variableName,
|
|
678
|
+
color: c.variable
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
tag: tags.definition(tags.variableName),
|
|
682
|
+
color: c.variable
|
|
683
|
+
},
|
|
684
|
+
{
|
|
685
|
+
tag: tags.propertyName,
|
|
686
|
+
color: c.variable
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
tag: tags.keyword,
|
|
690
|
+
color: c.keyword
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
tag: tags.definitionKeyword,
|
|
694
|
+
color: c.keyword
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
tag: tags.controlKeyword,
|
|
698
|
+
color: c.control
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
tag: tags.bool,
|
|
702
|
+
color: c.keyword
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
tag: tags.typeName,
|
|
706
|
+
color: c.type
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
tag: tags.definition(tags.typeName),
|
|
710
|
+
color: c.type
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
tag: tags.function(tags.variableName),
|
|
714
|
+
color: c.fn
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
tag: tags.function(tags.definition(tags.variableName)),
|
|
718
|
+
color: c.fn
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
tag: tags.number,
|
|
722
|
+
color: c.number
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
tag: tags.lineComment,
|
|
726
|
+
color: c.comment
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
tag: tags.blockComment,
|
|
730
|
+
color: c.comment
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
tag: tags.namespace,
|
|
734
|
+
color: c.modulePath
|
|
735
|
+
}
|
|
736
|
+
], { all: {
|
|
737
|
+
fontWeight: "normal",
|
|
738
|
+
fontStyle: "normal"
|
|
739
|
+
} }));
|
|
740
|
+
}
|
|
741
|
+
function getStyles() {
|
|
742
|
+
if (!cachedStyleSheet) {
|
|
743
|
+
cachedStyleSheet = new CSSStyleSheet();
|
|
744
|
+
cachedStyleSheet.replaceSync(WgslEdit_default);
|
|
745
|
+
}
|
|
746
|
+
return cachedStyleSheet;
|
|
747
|
+
}
|
|
748
|
+
/** Absorb instance properties set before custom element upgrade. */
|
|
749
|
+
function upgradeProperty(el, prop) {
|
|
750
|
+
if (Object.hasOwn(el, prop)) {
|
|
751
|
+
const value = el[prop];
|
|
752
|
+
delete el[prop];
|
|
753
|
+
el[prop] = value;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
/** Convert a module path or file path to a tab name: "package::main" -> "main", "main.wesl" -> "main.wesl" */
|
|
757
|
+
function toTabName(key) {
|
|
758
|
+
if (key.includes("::")) return key.replace(/^[^:]+::/, "").replaceAll("::", "/");
|
|
759
|
+
return key.replace(/^\.\//, "");
|
|
760
|
+
}
|
|
761
|
+
//#endregion
|
|
762
|
+
export { WgslEdit as t };
|