wgsl-edit 0.0.23 → 0.0.25
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 +4 -9
- package/dist/GpuValidator-BZSULsmE.js +48 -0
- package/dist/GpuValidator-D88-VTq9.js +48 -0
- package/dist/Language.d.ts +5 -1
- package/dist/Language.js +57 -50
- package/dist/{WgslEdit-CYb8MVnU.js → WgslEdit-ByXfb3R9.js} +96 -53
- package/dist/WgslEdit.d.ts +11 -18
- package/dist/WgslEdit.js +1 -1
- package/dist/index.js +1 -1
- package/dist/wgsl-edit.js +174 -123
- package/package.json +8 -6
- package/src/GpuValidator.ts +65 -0
- package/src/Language.ts +79 -66
- package/src/WgslEdit.ts +123 -79
- package/src/test/WgslEdit.e2e.ts +70 -0
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ Multiple `<script>` tags create a multi-file editor with tabs.
|
|
|
38
38
|
<wgsl-edit id="editor" theme="auto">
|
|
39
39
|
<script type="text/wesl">/* shader code */</script>
|
|
40
40
|
</wgsl-edit>
|
|
41
|
-
<wgsl-play
|
|
41
|
+
<wgsl-play from="editor"></wgsl-play>
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
The play component reads sources from the editor and live-previews the shader.
|
|
@@ -49,10 +49,6 @@ The play component reads sources from the editor and live-previews the shader.
|
|
|
49
49
|
const editor = document.querySelector("wgsl-edit");
|
|
50
50
|
|
|
51
51
|
editor.source = shaderCode; // set active file content
|
|
52
|
-
editor.sources = { // set all files
|
|
53
|
-
"package::main": mainCode,
|
|
54
|
-
"package::utils": utilsCode,
|
|
55
|
-
};
|
|
56
52
|
editor.addFile("helpers.wesl", code); // add a file
|
|
57
53
|
editor.activeFile = "helpers.wesl"; // switch tabs
|
|
58
54
|
|
|
@@ -82,9 +78,8 @@ editor.project = { // load a full project
|
|
|
82
78
|
### Properties
|
|
83
79
|
|
|
84
80
|
- `source: string` - Get/set active file content
|
|
85
|
-
- `sources: Record<string, string>` - Get/set all files (keyed by module path)
|
|
86
81
|
- `conditions: Record<string, boolean>` - Get/set conditions for conditional compilation (`@if`/`@elif`/`@else`)
|
|
87
|
-
- `project: WeslProject` -
|
|
82
|
+
- `project: WeslProject` - Get/set full project (weslSrc, conditions, constants, packageName, libs)
|
|
88
83
|
- `activeFile: string` - Get/set active file name
|
|
89
84
|
- `fileNames: string[]` - List all file names
|
|
90
85
|
- `theme`, `tabs`, `lint`, `lineNumbers`, `readonly`, `shaderRoot`, `fetchLibs` - Mirror attributes
|
|
@@ -98,13 +93,13 @@ editor.project = { // load a full project
|
|
|
98
93
|
|
|
99
94
|
### Events
|
|
100
95
|
|
|
101
|
-
- `change` - `
|
|
96
|
+
- `change` - `WeslProject` detail on edit or conditions change
|
|
102
97
|
- `file-change` - `{ action, file }` on add/remove/rename
|
|
103
98
|
|
|
104
99
|
## Using with wesl-plugin
|
|
105
100
|
|
|
106
101
|
For full project support (libraries, conditional compilation, constants),
|
|
107
|
-
use [wesl-plugin](https://github.com/wgsl-tooling-wg/wesl-js/tree/main/
|
|
102
|
+
use [wesl-plugin](https://github.com/wgsl-tooling-wg/wesl-js/tree/main/packages/wesl-plugin)
|
|
108
103
|
to assemble shaders at build time and pass them to the editor via `project`.
|
|
109
104
|
|
|
110
105
|
```typescript
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/GpuValidator.ts
|
|
2
|
+
/** Lazy-loaded GPU device singleton for shader validation. */
|
|
3
|
+
let device = null;
|
|
4
|
+
let initPromise = null;
|
|
5
|
+
let warned = false;
|
|
6
|
+
/** Get or initialize the shared GPU device, returning null if WebGPU is unavailable. */
|
|
7
|
+
async function getDevice() {
|
|
8
|
+
if (device) return device;
|
|
9
|
+
if (initPromise) return initPromise;
|
|
10
|
+
if (typeof navigator === "undefined" || !navigator.gpu) {
|
|
11
|
+
if (!warned) console.warn("wgsl-edit: WebGPU unavailable, GPU lint disabled");
|
|
12
|
+
warned = true;
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
initPromise = (async () => {
|
|
16
|
+
try {
|
|
17
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
18
|
+
if (!adapter) {
|
|
19
|
+
console.warn("wgsl-edit: no GPU adapter, GPU lint disabled");
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const dev = await adapter.requestDevice();
|
|
23
|
+
dev.lost.then(() => {
|
|
24
|
+
device = null;
|
|
25
|
+
initPromise = null;
|
|
26
|
+
});
|
|
27
|
+
device = dev;
|
|
28
|
+
return dev;
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.warn("wgsl-edit: GPU device request failed", e);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
})();
|
|
34
|
+
return initPromise;
|
|
35
|
+
}
|
|
36
|
+
/** Validate WGSL code via WebGPU createShaderModule + getCompilationInfo. */
|
|
37
|
+
async function validateWgsl(code) {
|
|
38
|
+
const dev = await getDevice();
|
|
39
|
+
if (!dev) return [];
|
|
40
|
+
return (await dev.createShaderModule({ code }).getCompilationInfo()).messages.filter((m) => m.type !== "info").map((m) => ({
|
|
41
|
+
offset: m.offset,
|
|
42
|
+
length: m.length,
|
|
43
|
+
message: m.message,
|
|
44
|
+
severity: m.type
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { validateWgsl };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/GpuValidator.ts
|
|
2
|
+
/** Lazy-loaded GPU device singleton for shader validation. */
|
|
3
|
+
let device = null;
|
|
4
|
+
let initPromise = null;
|
|
5
|
+
let warned = false;
|
|
6
|
+
/** Get or initialize the shared GPU device, returning null if WebGPU is unavailable. */
|
|
7
|
+
async function getDevice() {
|
|
8
|
+
if (device) return device;
|
|
9
|
+
if (initPromise) return initPromise;
|
|
10
|
+
if (typeof navigator === "undefined" || !navigator.gpu) {
|
|
11
|
+
if (!warned) console.warn("wgsl-edit: WebGPU unavailable, GPU lint disabled");
|
|
12
|
+
warned = true;
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
initPromise = (async () => {
|
|
16
|
+
try {
|
|
17
|
+
const adapter = await navigator.gpu.requestAdapter();
|
|
18
|
+
if (!adapter) {
|
|
19
|
+
console.warn("wgsl-edit: no GPU adapter, GPU lint disabled");
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const dev = await adapter.requestDevice();
|
|
23
|
+
dev.lost.then(() => {
|
|
24
|
+
device = null;
|
|
25
|
+
initPromise = null;
|
|
26
|
+
});
|
|
27
|
+
device = dev;
|
|
28
|
+
return dev;
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.warn("wgsl-edit: GPU device request failed", e);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
})();
|
|
34
|
+
return initPromise;
|
|
35
|
+
}
|
|
36
|
+
/** Validate WGSL code via WebGPU createShaderModule + getCompilationInfo. */
|
|
37
|
+
async function validateWgsl(code) {
|
|
38
|
+
const dev = await getDevice();
|
|
39
|
+
if (!dev) return [];
|
|
40
|
+
return (await dev.createShaderModule({ code }).getCompilationInfo()).messages.filter((m) => m.type !== "info").map((m) => ({
|
|
41
|
+
offset: m.offset,
|
|
42
|
+
length: m.length,
|
|
43
|
+
message: m.message,
|
|
44
|
+
severity: m.type
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { validateWgsl };
|
package/dist/Language.d.ts
CHANGED
|
@@ -21,10 +21,14 @@ interface WeslLintConfig {
|
|
|
21
21
|
fetchLibs?: (packageNames: string[]) => Promise<WeslBundle[]>;
|
|
22
22
|
/** Package names to ignore when checking unbound externals (e.g. virtual modules). */
|
|
23
23
|
ignorePackages?: () => string[];
|
|
24
|
+
/** GPU validation of linked WGSL. Called after WESL lint passes with no errors. */
|
|
25
|
+
gpuValidate?: () => Promise<Diagnostic[]>;
|
|
24
26
|
}
|
|
25
27
|
declare const weslLanguage: LRLanguage;
|
|
26
28
|
declare function wesl(): LanguageSupport;
|
|
27
29
|
/** Create a linter that validates WESL using the canonical parser. */
|
|
28
30
|
declare function createWeslLinter(config: WeslLintConfig): _codemirror_state0.Extension;
|
|
31
|
+
/** Lint once, fetch missing externals if needed, re-lint with new libs. @internal */
|
|
32
|
+
declare function lintAndFetch(config: WeslLintConfig): Promise<Diagnostic[]>;
|
|
29
33
|
//#endregion
|
|
30
|
-
export { WeslLintConfig, createWeslLinter, wesl, weslLanguage };
|
|
34
|
+
export { WeslLintConfig, createWeslLinter, lintAndFetch, wesl, weslLanguage };
|
package/dist/Language.js
CHANGED
|
@@ -21,15 +21,33 @@ function wesl() {
|
|
|
21
21
|
function createWeslLinter(config) {
|
|
22
22
|
return linter(async () => lintAndFetch(config), { delay: 300 });
|
|
23
23
|
}
|
|
24
|
-
/** Lint once, fetch missing externals if needed, re-lint with new libs. */
|
|
24
|
+
/** Lint once, fetch missing externals if needed, re-lint with new libs. @internal */
|
|
25
25
|
async function lintAndFetch(config) {
|
|
26
26
|
const libs = config.getLibs?.() ?? [];
|
|
27
27
|
const ignored = new Set(config.ignorePackages?.() ?? []);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
let result = lintPass(config, libs, ignored);
|
|
29
|
+
let { diagnostics, externals, weslErrorCount } = result;
|
|
30
|
+
if (config.fetchLibs && externals.length) {
|
|
31
|
+
const newLibs = await config.fetchLibs(externals);
|
|
32
|
+
if (newLibs.length) {
|
|
33
|
+
result = lintPass(config, [...libs, ...newLibs], ignored);
|
|
34
|
+
({diagnostics, weslErrorCount} = result);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (weslErrorCount === 0 && config.gpuValidate) try {
|
|
38
|
+
const gpuDiags = await config.gpuValidate();
|
|
39
|
+
diagnostics.push(...gpuDiags);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
const msg = e instanceof Error ? e.message : "unknown error";
|
|
42
|
+
diagnostics.push({
|
|
43
|
+
from: 0,
|
|
44
|
+
to: 0,
|
|
45
|
+
severity: "warning",
|
|
46
|
+
message: `GPU validation skipped: ${msg}`,
|
|
47
|
+
source: "WebGPU"
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return diagnostics;
|
|
33
51
|
}
|
|
34
52
|
/** Parse, bind, collect diagnostics and discover missing externals. */
|
|
35
53
|
function lintPass(config, libs, ignored) {
|
|
@@ -37,82 +55,71 @@ function lintPass(config, libs, ignored) {
|
|
|
37
55
|
const rootModule = config.rootModule();
|
|
38
56
|
const diagnostics = [];
|
|
39
57
|
let externals = [];
|
|
58
|
+
let weslErrorCount = 0;
|
|
40
59
|
try {
|
|
41
60
|
const resolver = buildResolver(sources, libs, config.packageName?.());
|
|
42
61
|
const rootAst = resolver.resolveModule(rootModule);
|
|
43
62
|
if (rootAst) {
|
|
44
|
-
const result =
|
|
45
|
-
|
|
63
|
+
const result = bindIdents({
|
|
64
|
+
resolver,
|
|
65
|
+
rootAst,
|
|
66
|
+
conditions: config.conditions?.(),
|
|
67
|
+
accumulateUnbound: true
|
|
68
|
+
});
|
|
69
|
+
const unbound = unboundDiagnostics(result, rootModule, ignored);
|
|
70
|
+
diagnostics.push(...unbound);
|
|
71
|
+
weslErrorCount = unbound.length;
|
|
46
72
|
externals = findMissingPackages(rootAst, result, resolver, ignored, libs);
|
|
47
73
|
}
|
|
48
74
|
} catch (e) {
|
|
49
75
|
const diag = errorToDiagnostic(e);
|
|
50
|
-
if (diag)
|
|
76
|
+
if (diag) {
|
|
77
|
+
diagnostics.push(diag);
|
|
78
|
+
weslErrorCount++;
|
|
79
|
+
}
|
|
51
80
|
}
|
|
52
81
|
diagnostics.push(...config.getExternalDiagnostics?.() ?? []);
|
|
53
82
|
return {
|
|
54
83
|
diagnostics,
|
|
55
|
-
externals
|
|
84
|
+
externals,
|
|
85
|
+
weslErrorCount
|
|
56
86
|
};
|
|
57
87
|
}
|
|
58
|
-
/** Build a resolver from sources and optional libs. */
|
|
59
88
|
function buildResolver(sources, libs, packageName) {
|
|
60
89
|
const record = new RecordResolver(sources, { packageName });
|
|
61
90
|
if (libs.length === 0) return record;
|
|
62
91
|
return new CompositeResolver([record, ...libs.map((b) => new BundleResolver(b))]);
|
|
63
92
|
}
|
|
64
|
-
/** Parse and bind identifiers starting from a root module. */
|
|
65
|
-
function runBind(config, resolver, rootAst) {
|
|
66
|
-
return bindIdents({
|
|
67
|
-
resolver,
|
|
68
|
-
rootAst,
|
|
69
|
-
conditions: config.conditions?.(),
|
|
70
|
-
accumulateUnbound: true
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
93
|
/** Convert unbound refs to diagnostics, skipping ignored packages. */
|
|
74
94
|
function unboundDiagnostics(result, rootModule, ignored) {
|
|
75
|
-
|
|
76
|
-
for (const ref of result.unbound ?? []) {
|
|
77
|
-
if (ref.srcModule.modulePath !== rootModule) continue;
|
|
78
|
-
if (ref.path.length > 1 && ignored.has(ref.path[0])) continue;
|
|
79
|
-
diagnostics.push(unboundToDiagnostic(ref));
|
|
80
|
-
}
|
|
81
|
-
return diagnostics;
|
|
95
|
+
return (result.unbound ?? []).filter((ref) => ref.srcModule.modulePath === rootModule && !(ref.path.length > 1 && ignored.has(ref.path[0]))).map(unboundToDiagnostic);
|
|
82
96
|
}
|
|
83
|
-
/** Convert a WESL error to a CodeMirror diagnostic. */
|
|
84
97
|
function errorToDiagnostic(e) {
|
|
85
|
-
if (e instanceof WeslParseError)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
98
|
+
if (!(e instanceof WeslParseError)) return void 0;
|
|
99
|
+
const [from, to] = e.span;
|
|
100
|
+
return {
|
|
101
|
+
from,
|
|
102
|
+
to,
|
|
103
|
+
severity: "error",
|
|
104
|
+
message: e.cause?.message ?? e.message
|
|
105
|
+
};
|
|
94
106
|
}
|
|
95
107
|
/** Find external package names not yet loaded, from unresolved imports and unbound refs. */
|
|
96
108
|
function findMissingPackages(rootAst, result, resolver, ignored, libs) {
|
|
97
109
|
const loaded = new Set(libs.map((b) => b.name));
|
|
98
|
-
const
|
|
99
|
-
|
|
110
|
+
const skip = (r) => !isExternalRoot(r) || ignored.has(r) || loaded.has(r);
|
|
111
|
+
const fromImports = rootAst.imports.filter((imp) => {
|
|
100
112
|
const root = imp.segments[0]?.name;
|
|
101
|
-
if (!root ||
|
|
113
|
+
if (!root || skip(root)) return false;
|
|
102
114
|
const modPath = imp.segments.map((s) => s.name).join("::");
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (ref.path.length > 1 && isExternalRoot(root) && !ignored.has(root) && !loaded.has(root)) pkgs.push(root);
|
|
108
|
-
}
|
|
109
|
-
return [...new Set(pkgs)];
|
|
115
|
+
return !resolver.resolveModule(modPath);
|
|
116
|
+
}).map((imp) => imp.segments[0].name);
|
|
117
|
+
const fromUnbound = (result.unbound ?? []).filter((ref) => ref.path.length > 1 && !skip(ref.path[0])).map((ref) => ref.path[0]);
|
|
118
|
+
return [...new Set([...fromImports, ...fromUnbound])];
|
|
110
119
|
}
|
|
111
|
-
/** @return true if root is an external package name (not package/super). */
|
|
112
120
|
function isExternalRoot(root) {
|
|
113
121
|
return root !== "package" && root !== "super";
|
|
114
122
|
}
|
|
115
|
-
/** Convert an unbound reference to a CodeMirror diagnostic. */
|
|
116
123
|
function unboundToDiagnostic(ref) {
|
|
117
124
|
return {
|
|
118
125
|
from: ref.start,
|
|
@@ -122,4 +129,4 @@ function unboundToDiagnostic(ref) {
|
|
|
122
129
|
};
|
|
123
130
|
}
|
|
124
131
|
//#endregion
|
|
125
|
-
export { createWeslLinter, wesl, weslLanguage };
|
|
132
|
+
export { createWeslLinter, lintAndFetch, wesl, weslLanguage };
|
|
@@ -46,7 +46,8 @@ var WgslEdit = class extends HTMLElement {
|
|
|
46
46
|
"lint",
|
|
47
47
|
"lint-from",
|
|
48
48
|
"line-numbers",
|
|
49
|
-
"fetch-libs"
|
|
49
|
+
"fetch-libs",
|
|
50
|
+
"gpu-lint"
|
|
50
51
|
];
|
|
51
52
|
editorView = null;
|
|
52
53
|
editorContainer;
|
|
@@ -65,8 +66,10 @@ var WgslEdit = class extends HTMLElement {
|
|
|
65
66
|
_rootModuleName;
|
|
66
67
|
_tabs = true;
|
|
67
68
|
_lint = "on";
|
|
69
|
+
_gpuLint = true;
|
|
68
70
|
_fetchLibs = true;
|
|
69
71
|
_conditions = {};
|
|
72
|
+
_constants;
|
|
70
73
|
_packageName;
|
|
71
74
|
_libs = [];
|
|
72
75
|
_ignorePackages = ["constants", "env"];
|
|
@@ -123,6 +126,9 @@ var WgslEdit = class extends HTMLElement {
|
|
|
123
126
|
} else if (name === "fetch-libs") {
|
|
124
127
|
this._fetchLibs = value !== "false";
|
|
125
128
|
this.updateLint();
|
|
129
|
+
} else if (name === "gpu-lint") {
|
|
130
|
+
this._gpuLint = value !== "off";
|
|
131
|
+
this.updateLint();
|
|
126
132
|
}
|
|
127
133
|
}
|
|
128
134
|
/** Conditions for conditional compilation (@if/@elif/@else). */
|
|
@@ -177,10 +183,21 @@ var WgslEdit = class extends HTMLElement {
|
|
|
177
183
|
if (firstKey) this.switchToFile(toTabName(firstKey));
|
|
178
184
|
this.renderTabs();
|
|
179
185
|
}
|
|
180
|
-
|
|
186
|
+
get project() {
|
|
187
|
+
return {
|
|
188
|
+
weslSrc: this.sources,
|
|
189
|
+
rootModuleName: this._rootModuleName,
|
|
190
|
+
conditions: this._conditions,
|
|
191
|
+
constants: this._constants,
|
|
192
|
+
libs: this._libs,
|
|
193
|
+
packageName: this._packageName
|
|
194
|
+
};
|
|
195
|
+
}
|
|
181
196
|
set project(value) {
|
|
182
|
-
const { weslSrc, rootModuleName, conditions
|
|
197
|
+
const { weslSrc, rootModuleName, conditions } = value;
|
|
198
|
+
const { constants, packageName, libs } = value;
|
|
183
199
|
if (conditions !== void 0) this._conditions = conditions;
|
|
200
|
+
if (constants !== void 0) this._constants = constants;
|
|
184
201
|
if (packageName !== void 0) this._packageName = packageName;
|
|
185
202
|
if (libs !== void 0) this._libs = libs;
|
|
186
203
|
if (rootModuleName !== void 0) this._rootModuleName = rootModuleName;
|
|
@@ -193,18 +210,22 @@ var WgslEdit = class extends HTMLElement {
|
|
|
193
210
|
}
|
|
194
211
|
/** Link/compile WESL sources into WGSL. Returns the compiled WGSL string. */
|
|
195
212
|
async link(options) {
|
|
196
|
-
const pkg = this._packageName ?? "package";
|
|
197
|
-
const rootModuleName = this._rootModuleName ?? fileToModulePath(this._activeFile, pkg, false);
|
|
198
213
|
return (await link({
|
|
214
|
+
...this.linkParams(),
|
|
215
|
+
...options
|
|
216
|
+
})).dest;
|
|
217
|
+
}
|
|
218
|
+
linkParams() {
|
|
219
|
+
const pkg = this._packageName ?? "package";
|
|
220
|
+
return {
|
|
199
221
|
weslSrc: this.sources,
|
|
200
|
-
rootModuleName,
|
|
222
|
+
rootModuleName: this._rootModuleName ?? fileToModulePath(this._activeFile, pkg, false),
|
|
201
223
|
conditions: this._conditions,
|
|
224
|
+
constants: this._constants,
|
|
202
225
|
libs: this._libs,
|
|
203
|
-
packageName: pkg
|
|
204
|
-
|
|
205
|
-
})).dest;
|
|
226
|
+
packageName: pkg
|
|
227
|
+
};
|
|
206
228
|
}
|
|
207
|
-
/** Library bundles for linking (set via project). */
|
|
208
229
|
get libs() {
|
|
209
230
|
return this._libs;
|
|
210
231
|
}
|
|
@@ -216,19 +237,15 @@ var WgslEdit = class extends HTMLElement {
|
|
|
216
237
|
this._rootModuleName = value;
|
|
217
238
|
this.dispatchChange();
|
|
218
239
|
}
|
|
219
|
-
/** Currently active file name (selected tab). */
|
|
220
240
|
get activeFile() {
|
|
221
241
|
return this._activeFile;
|
|
222
242
|
}
|
|
223
|
-
/** Switch to a file by name. */
|
|
224
243
|
set activeFile(name) {
|
|
225
244
|
this.switchToFile(name);
|
|
226
245
|
}
|
|
227
|
-
/** List of file names in order. */
|
|
228
246
|
get fileNames() {
|
|
229
247
|
return Array.from(this._files.keys());
|
|
230
248
|
}
|
|
231
|
-
/** Tab bar visibility. */
|
|
232
249
|
get tabs() {
|
|
233
250
|
return this._tabs;
|
|
234
251
|
}
|
|
@@ -253,6 +270,15 @@ var WgslEdit = class extends HTMLElement {
|
|
|
253
270
|
if (value) this.setAttribute("line-numbers", "true");
|
|
254
271
|
else this.removeAttribute("line-numbers");
|
|
255
272
|
}
|
|
273
|
+
/** GPU validation of linked WGSL (default: true). Set to false to disable. */
|
|
274
|
+
get gpuLint() {
|
|
275
|
+
return this._gpuLint;
|
|
276
|
+
}
|
|
277
|
+
set gpuLint(value) {
|
|
278
|
+
this._gpuLint = value;
|
|
279
|
+
if (!value) this.setAttribute("gpu-lint", "off");
|
|
280
|
+
else this.removeAttribute("gpu-lint");
|
|
281
|
+
}
|
|
256
282
|
/** Whether to auto-fetch missing library packages from npm (default: true). */
|
|
257
283
|
get fetchLibs() {
|
|
258
284
|
return this._fetchLibs;
|
|
@@ -262,7 +288,6 @@ var WgslEdit = class extends HTMLElement {
|
|
|
262
288
|
if (value) this.removeAttribute("fetch-libs");
|
|
263
289
|
else this.setAttribute("fetch-libs", "false");
|
|
264
290
|
}
|
|
265
|
-
/** Whether the editor is currently loading content. */
|
|
266
291
|
get loading() {
|
|
267
292
|
return this.snackbar.classList.contains("visible");
|
|
268
293
|
}
|
|
@@ -303,7 +328,6 @@ var WgslEdit = class extends HTMLElement {
|
|
|
303
328
|
if (value) this.setAttribute("shader-root", value);
|
|
304
329
|
else this.removeAttribute("shader-root");
|
|
305
330
|
}
|
|
306
|
-
/** Add a new file. */
|
|
307
331
|
addFile(name, content = "") {
|
|
308
332
|
if (this._files.has(name)) return;
|
|
309
333
|
this._files.set(name, { doc: Text.of(content.split("\n")) });
|
|
@@ -311,7 +335,6 @@ var WgslEdit = class extends HTMLElement {
|
|
|
311
335
|
this.renderTabs();
|
|
312
336
|
this.dispatchFileChange("add", name);
|
|
313
337
|
}
|
|
314
|
-
/** Remove a file. */
|
|
315
338
|
removeFile(name) {
|
|
316
339
|
if (!this._files.has(name) || this._files.size <= 1) return;
|
|
317
340
|
this._files.delete(name);
|
|
@@ -322,7 +345,6 @@ var WgslEdit = class extends HTMLElement {
|
|
|
322
345
|
this.renderTabs();
|
|
323
346
|
this.dispatchFileChange("remove", name);
|
|
324
347
|
}
|
|
325
|
-
/** Rename a file. */
|
|
326
348
|
renameFile(oldName, newName) {
|
|
327
349
|
const state = this._files.get(oldName);
|
|
328
350
|
if (!state || this._files.has(newName)) return;
|
|
@@ -353,32 +375,23 @@ var WgslEdit = class extends HTMLElement {
|
|
|
353
375
|
}
|
|
354
376
|
this.renderTabs();
|
|
355
377
|
}
|
|
356
|
-
/** Save current editor state to the active file. */
|
|
357
378
|
saveCurrentFileState() {
|
|
358
|
-
|
|
359
|
-
const state = this._files.get(this._activeFile);
|
|
360
|
-
if (state)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
379
|
+
const view = this.editorView;
|
|
380
|
+
const state = this._activeFile ? this._files.get(this._activeFile) : void 0;
|
|
381
|
+
if (!view || !state) return;
|
|
382
|
+
state.doc = view.state.doc;
|
|
383
|
+
state.selection = view.state.selection;
|
|
384
|
+
state.scrollPos = view.scrollDOM.scrollTop;
|
|
365
385
|
}
|
|
366
386
|
dispatchChange() {
|
|
367
|
-
|
|
368
|
-
const detail = {
|
|
369
|
-
source,
|
|
370
|
-
sources,
|
|
371
|
-
rootModuleName,
|
|
372
|
-
conditions,
|
|
373
|
-
libs
|
|
374
|
-
};
|
|
375
|
-
this.dispatchEvent(new CustomEvent("change", { detail }));
|
|
387
|
+
this.dispatchEvent(new CustomEvent("change", { detail: this.project }));
|
|
376
388
|
}
|
|
377
389
|
dispatchFileChange(action, file) {
|
|
378
|
-
|
|
390
|
+
const detail = {
|
|
379
391
|
action,
|
|
380
392
|
file
|
|
381
|
-
}
|
|
393
|
+
};
|
|
394
|
+
this.dispatchEvent(new CustomEvent("file-change", { detail }));
|
|
382
395
|
}
|
|
383
396
|
initEditor() {
|
|
384
397
|
this.readInitialAttributes();
|
|
@@ -393,10 +406,11 @@ var WgslEdit = class extends HTMLElement {
|
|
|
393
406
|
this._files.set("main.wesl", { doc: Text.of(initialDoc.split("\n")) });
|
|
394
407
|
this._activeFile = "main.wesl";
|
|
395
408
|
}
|
|
409
|
+
const extensions = this.buildExtensions();
|
|
396
410
|
this.editorView = new EditorView({
|
|
397
411
|
state: EditorState.create({
|
|
398
412
|
doc: initialDoc,
|
|
399
|
-
extensions
|
|
413
|
+
extensions
|
|
400
414
|
}),
|
|
401
415
|
parent: this.editorContainer
|
|
402
416
|
});
|
|
@@ -471,11 +485,13 @@ var WgslEdit = class extends HTMLElement {
|
|
|
471
485
|
this.editorView?.dispatch({ effects: this.themeCompartment.reconfigure(this.resolveTheme()) });
|
|
472
486
|
}
|
|
473
487
|
updateReadonly() {
|
|
474
|
-
|
|
488
|
+
const ext = EditorState.readOnly.of(this.readonly);
|
|
489
|
+
this.editorView?.dispatch({ effects: this.readonlyCompartment.reconfigure(ext) });
|
|
475
490
|
this.renderTabs();
|
|
476
491
|
}
|
|
477
492
|
resolveLint() {
|
|
478
493
|
if (this._lint === "off") return [];
|
|
494
|
+
const useGpuLint = this._gpuLint && !this._lintFromEl;
|
|
479
495
|
return createWeslLinter({
|
|
480
496
|
getSources: () => this.sources,
|
|
481
497
|
rootModule: () => fileToModulePath(this._activeFile, this._packageName ?? "package", false),
|
|
@@ -484,12 +500,22 @@ var WgslEdit = class extends HTMLElement {
|
|
|
484
500
|
getExternalDiagnostics: () => this._externalDiagnostics,
|
|
485
501
|
getLibs: () => this._libs,
|
|
486
502
|
fetchLibs: this._fetchLibs ? (pkgs) => this.fetchLibsOnDemand(pkgs) : void 0,
|
|
487
|
-
ignorePackages: () => this._ignorePackages
|
|
503
|
+
ignorePackages: () => this._ignorePackages,
|
|
504
|
+
gpuValidate: useGpuLint ? () => this.gpuValidate() : void 0
|
|
488
505
|
});
|
|
489
506
|
}
|
|
507
|
+
/** Link WESL->WGSL and validate via WebGPU, returning CodeMirror diagnostics. */
|
|
508
|
+
async gpuValidate() {
|
|
509
|
+
const { validateWgsl } = await import("./GpuValidator-BZSULsmE.js");
|
|
510
|
+
const params = this.linkParams();
|
|
511
|
+
const linked = await link(params);
|
|
512
|
+
const messages = await validateWgsl(linked.dest);
|
|
513
|
+
const pkg = params.packageName ?? "package";
|
|
514
|
+
return mapGpuDiagnostics(messages, linked, this._activeFile, pkg);
|
|
515
|
+
}
|
|
490
516
|
/** Fetch missing library packages, deduplicating in-flight requests. */
|
|
491
|
-
async fetchLibsOnDemand(
|
|
492
|
-
const needed =
|
|
517
|
+
async fetchLibsOnDemand(names) {
|
|
518
|
+
const needed = names.filter((n) => !this._fetchedPkgs.has(n) && !this._fetchingPkgs.has(n));
|
|
493
519
|
if (needed.length === 0) return [];
|
|
494
520
|
for (const n of needed) this._fetchingPkgs.add(n);
|
|
495
521
|
this.showSnack(`Loading ${needed.join(", ")}…`);
|
|
@@ -516,18 +542,20 @@ var WgslEdit = class extends HTMLElement {
|
|
|
516
542
|
}
|
|
517
543
|
/** Listen for compile-error/compile-success events from a lint source element. */
|
|
518
544
|
connectLintSource(id) {
|
|
545
|
+
const hadExternal = !!this._lintFromEl;
|
|
519
546
|
if (this._lintFromEl) {
|
|
520
547
|
this._lintFromEl.removeEventListener("compile-error", this._boundCompileError);
|
|
521
548
|
this._lintFromEl.removeEventListener("compile-success", this._boundCompileSuccess);
|
|
522
549
|
this._lintFromEl = null;
|
|
523
550
|
}
|
|
524
551
|
this._externalDiagnostics = [];
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
552
|
+
const el = id ? document.getElementById(id) : null;
|
|
553
|
+
if (el) {
|
|
554
|
+
this._lintFromEl = el;
|
|
555
|
+
el.addEventListener("compile-error", this._boundCompileError);
|
|
556
|
+
el.addEventListener("compile-success", this._boundCompileSuccess);
|
|
557
|
+
}
|
|
558
|
+
if (hadExternal !== !!this._lintFromEl) this.updateLint();
|
|
531
559
|
}
|
|
532
560
|
onCompileError(e) {
|
|
533
561
|
const detail = e.detail;
|
|
@@ -560,7 +588,8 @@ var WgslEdit = class extends HTMLElement {
|
|
|
560
588
|
return this._lineNumbers ? [] : EditorView.theme({ ".cm-gutters": { display: "none" } });
|
|
561
589
|
}
|
|
562
590
|
updateLineNumbers() {
|
|
563
|
-
|
|
591
|
+
const ext = this.resolveLineNumbers();
|
|
592
|
+
this.editorView?.dispatch({ effects: this.lineNumbersCompartment.reconfigure(ext) });
|
|
564
593
|
}
|
|
565
594
|
/** Parse script tags into _files. Supports single or multi-file via data-name. */
|
|
566
595
|
parseInlineContent() {
|
|
@@ -576,7 +605,6 @@ var WgslEdit = class extends HTMLElement {
|
|
|
576
605
|
this._files.set(name, { doc: Text.of(content.split("\n")) });
|
|
577
606
|
}
|
|
578
607
|
}
|
|
579
|
-
/** Render tab bar based on files and visibility mode. */
|
|
580
608
|
renderTabs() {
|
|
581
609
|
this.tabBar.style.display = this._tabs ? "flex" : "none";
|
|
582
610
|
if (!this._tabs) return;
|
|
@@ -584,7 +612,6 @@ var WgslEdit = class extends HTMLElement {
|
|
|
584
612
|
for (const name of this._files.keys()) this.tabBar.appendChild(this.createTab(name));
|
|
585
613
|
if (!this.readonly) this.tabBar.appendChild(this.createAddButton());
|
|
586
614
|
}
|
|
587
|
-
/** Create a tab button for a file. */
|
|
588
615
|
createTab(name) {
|
|
589
616
|
const tab = document.createElement("button");
|
|
590
617
|
tab.className = "tab" + (name === this._activeFile ? " active" : "");
|
|
@@ -608,7 +635,6 @@ var WgslEdit = class extends HTMLElement {
|
|
|
608
635
|
tab.addEventListener("click", () => this.switchToFile(name));
|
|
609
636
|
return tab;
|
|
610
637
|
}
|
|
611
|
-
/** Create the "+" button for adding new files. */
|
|
612
638
|
createAddButton() {
|
|
613
639
|
const btn = document.createElement("button");
|
|
614
640
|
btn.className = "tab-add";
|
|
@@ -621,7 +647,6 @@ var WgslEdit = class extends HTMLElement {
|
|
|
621
647
|
});
|
|
622
648
|
return btn;
|
|
623
649
|
}
|
|
624
|
-
/** Start inline rename of a tab. */
|
|
625
650
|
startRenameTab(tab, nameSpan, oldName) {
|
|
626
651
|
const input = document.createElement("input");
|
|
627
652
|
input.className = "tab-rename";
|
|
@@ -738,6 +763,24 @@ function weslColors(c) {
|
|
|
738
763
|
fontStyle: "normal"
|
|
739
764
|
} }));
|
|
740
765
|
}
|
|
766
|
+
/** Map GPU validation messages back to source positions via the source map. */
|
|
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
|
+
}
|
|
741
784
|
function getStyles() {
|
|
742
785
|
if (!cachedStyleSheet) {
|
|
743
786
|
cachedStyleSheet = new CSSStyleSheet();
|