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.
@@ -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
- upgradeProperty(this, "conditions");
103
- upgradeProperty(this, "source");
104
- upgradeProperty(this, "sources");
105
- upgradeProperty(this, "project");
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
- if (name === "src" && value && this.editorView) this.loadFromUrl(value);
114
- else if (name === "readonly") this.updateReadonly();
115
- else if (name === "theme") this.theme = value || "auto";
116
- else if (name === "tabs") {
117
- this._tabs = value !== "false";
118
- this.renderTabs();
119
- } else if (name === "lint") {
120
- this._lint = value || "on";
121
- this.updateLint();
122
- } else if (name === "lint-from") this.connectLintSource(value);
123
- else if (name === "line-numbers") {
124
- this._lineNumbers = value === "true";
125
- this.updateLineNumbers();
126
- } else if (name === "fetch-libs") {
127
- this._fetchLibs = value !== "false";
128
- this.updateLint();
129
- } else if (name === "gpu-lint") {
130
- this._gpuLint = value !== "off";
131
- this.updateLint();
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: Text.of(value.split("\n")) });
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 to = this.editorView.state.doc.length;
156
- this.editorView.dispatch({ changes: {
157
- from: 0,
158
- to,
159
- insert: value
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 = Text.of(value.split("\n"));
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._packageName ?? "package";
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: Text.of(content.split("\n")) });
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(rootModuleName ?? this._rootModuleName ?? "");
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 ?? fileToModulePath(this._activeFile, pkg, false),
261
+ rootModuleName: this._rootModuleName ?? this.activeModulePath(),
223
262
  conditions: this._conditions,
224
263
  constants: this._constants,
225
264
  libs: this._libs,
226
- packageName: pkg
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: Text.of(content.split("\n")) });
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
- if (this.editorView) {
364
- const changes = {
365
- from: 0,
366
- to: this.editorView.state.doc.length,
367
- insert: fileState.doc.toString()
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 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;
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 initialDoc = this._pendingSource ?? (firstFile ? this._files.get(firstFile).doc.toString() : "");
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: Text.of(initialDoc.split("\n")) });
529
+ this._files.set("main.wesl", { doc: toDoc(initialDoc) });
407
530
  this._activeFile = "main.wesl";
408
531
  }
409
- const extensions = this.buildExtensions();
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: EditorState.create({
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.editorView?.dispatch({ effects: this.themeCompartment.reconfigure(this.resolveTheme()) });
612
+ this.reconfigure(this.themeCompartment, this.resolveTheme());
486
613
  }
487
614
  updateReadonly() {
488
615
  const ext = EditorState.readOnly.of(this.readonly);
489
- this.editorView?.dispatch({ effects: this.readonlyCompartment.reconfigure(ext) });
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: () => fileToModulePath(this._activeFile, this._packageName ?? "package", false),
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 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);
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.editorView?.dispatch({ effects: this.lintCompartment.reconfigure(this.resolveLint()) });
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
- if (this._lintFromEl) {
547
- this._lintFromEl.removeEventListener("compile-error", this._boundCompileError);
548
- this._lintFromEl.removeEventListener("compile-success", this._boundCompileSuccess);
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._packageName ?? "package";
565
- const activeModule = fileToModulePath(this._activeFile, pkg, false);
566
- this._externalDiagnostics = detail.locations.filter((loc) => {
567
- if (!loc.file) return true;
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: Math.min(from + (loc.length ?? 1), doc.length),
575
- severity: loc.severity,
576
- message: loc.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
- const ext = this.resolveLineNumbers();
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: Text.of(content.split("\n")) });
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: Text.of(content.split("\n")) });
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
- /** 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
- }
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 };