EHS-Studio Claude Sonnet 5.5 commited on
Commit
ee84700
·
1 Parent(s): 8181411

Extension 0.12.1: found and fixed by running it in a real VSCodium

Browse files

One-line rule completions, property words sorted first, designer knows the wood and mirror settings; new real-editor test suites.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

.gitignore CHANGED
@@ -61,3 +61,6 @@ mini-services/
61
 
62
  # local screenshots, never published
63
  .zscripts/
 
 
 
 
61
 
62
  # local screenshots, never published
63
  .zscripts/
64
+
65
+ # real-editor test scratch (scripts/test-in-editor.mjs)
66
+ /.vsc-test/
GUIDE.md CHANGED
@@ -729,7 +729,7 @@ Three ways to get it:
729
 
730
  1. VSCodium: open the Extensions tab, search "DSSL" and install. It is on Open VSX:
731
  https://open-vsx.org/extension/ehs-studio/dssl-vscode
732
- 2. VS Code: download `dssl-vscode/dssl-vscode-0.12.0.vsix` from the repo files, then
733
  Extensions panel, `...`, Install from VSIX. (The Microsoft Marketplace listing is not published yet.)
734
  3. Build it yourself: `cd dssl-vscode && npx @vscode/vsce package --no-dependencies`.
735
 
@@ -799,6 +799,9 @@ node scripts/test-browser-v090.mjs # 0.9.0 in Chrome: pixels, lifecycle, re
799
  node scripts/test-v0100.mjs # 0.10.0: glass, tilt, named-colour alpha, playground examples, HTML filter
800
  node scripts/test-designer.mjs # the designer's state to DSSL and back, plus a random sweep
801
  node scripts/test-designer-browser.mjs # the designer panel, driven like a person would
 
 
 
802
  node scripts/test-designer-host.mjs # the extension's wiring, against a fake of the editor API
803
  node scripts/test-v082.mjs # 0.8.2: one cascade across stylesheets, fluid longhands
804
  node scripts/test-v081.mjs # 0.8.1: fluid math, raw blocks, packaged CLI
@@ -827,6 +830,10 @@ Read this before choosing DSSL for a real site.
827
  JS (`@signal`, `animate:`, deform, overlap) stays out and is counted in the
828
  report, so you always know what a no-JS page gives up. If a page must work
829
  without JS AND uses the dynamic features, use CSS.
 
 
 
 
830
  - **First paint.** Styles are applied by the engine after it loads, so large
831
  pages can flash unstyled content (CLS). The build output fixes this for
832
  static pages (real CSS paints before any JS). For live pages, the docs site
 
729
 
730
  1. VSCodium: open the Extensions tab, search "DSSL" and install. It is on Open VSX:
731
  https://open-vsx.org/extension/ehs-studio/dssl-vscode
732
+ 2. VS Code: download `dssl-vscode/dssl-vscode-0.12.1.vsix` from the repo files, then
733
  Extensions panel, `...`, Install from VSIX. (The Microsoft Marketplace listing is not published yet.)
734
  3. Build it yourself: `cd dssl-vscode && npx @vscode/vsce package --no-dependencies`.
735
 
 
799
  node scripts/test-v0100.mjs # 0.10.0: glass, tilt, named-colour alpha, playground examples, HTML filter
800
  node scripts/test-designer.mjs # the designer's state to DSSL and back, plus a random sweep
801
  node scripts/test-designer-browser.mjs # the designer panel, driven like a person would
802
+ node scripts/test-in-editor.mjs # the extension inside a REAL VS Code or VSCodium: diagnostics, completions, hover, commands (needs a display)
803
+ node scripts/test-editor-ui.mjs # and clicking through that real editor: typing, hover, the designer, Insert at cursor
804
+ node scripts/test-all.mjs --editor # everything above, plus the two real-editor suites
805
  node scripts/test-designer-host.mjs # the extension's wiring, against a fake of the editor API
806
  node scripts/test-v082.mjs # 0.8.2: one cascade across stylesheets, fluid longhands
807
  node scripts/test-v081.mjs # 0.8.1: fluid math, raw blocks, packaged CLI
 
830
  JS (`@signal`, `animate:`, deform, overlap) stays out and is counted in the
831
  report, so you always know what a no-JS page gives up. If a page must work
832
  without JS AND uses the dynamic features, use CSS.
833
+ - **The editor extension is tested in one editor.** It runs in a real VSCodium (1.135, Linux), driven both through the
834
+ extension host and by clicking through the window (`test-in-editor.mjs`, `test-editor-ui.mjs`). The Microsoft build of
835
+ VS Code, Windows and macOS share the same extension API but have not been run. Those suites need a display and an
836
+ editor, so they are not part of `test-all.mjs` unless you pass `--editor`.
837
  - **First paint.** Styles are applied by the engine after it loads, so large
838
  pages can flash unstyled content (CLS). The build output fixes this for
839
  static pages (real CSS paints before any JS). For live pages, the docs site
README.md CHANGED
@@ -68,7 +68,7 @@ Both drop a `dssl-engine/` folder next to you: the engine, a starter token set,
68
 
69
  Prefer to click: [dssl-engine.zip](https://dssl-sable.vercel.app/dssl-engine.zip) or [dssl-engine.tar.gz](https://dssl-sable.vercel.app/dssl-engine.tar.gz). Same package, straight from the browser.
70
 
71
- Writing your own .dssl? The official editor extension (`ehs-studio.dssl-vscode`) puts the real compiler in your editor: the squiggles are the same errors the browser console prints, completions come from your own token registry, and hovers explain the semantic vocabulary. One package for VS Code and VSCodium (same extension API). In VSCodium, search "DSSL" in the Extensions tab (it is on Open VSX: https://open-vsx.org/extension/ehs-studio/dssl-vscode). In VS Code, download `dssl-vscode/dssl-vscode-0.12.0.vsix` from the repo files, then Extensions panel -> ... -> Install from VSIX. See `dssl-vscode/PUBLISHING.md` for publishing.
72
 
73
  AI agents and machine reviewers: this repo ships `llms.txt` (dense, structured facts), `AGENTS.md` (how to work on the codebase) and `PARITY.md` (the honest feature matrix against CSS). The site serves the same `llms.txt` at https://dssl-sable.vercel.app/llms.txt
74
 
 
68
 
69
  Prefer to click: [dssl-engine.zip](https://dssl-sable.vercel.app/dssl-engine.zip) or [dssl-engine.tar.gz](https://dssl-sable.vercel.app/dssl-engine.tar.gz). Same package, straight from the browser.
70
 
71
+ Writing your own .dssl? The official editor extension (`ehs-studio.dssl-vscode`) puts the real compiler in your editor: the squiggles are the same errors the browser console prints, completions come from your own token registry, and hovers explain the semantic vocabulary. One package for VS Code and VSCodium (same extension API). In VSCodium, search "DSSL" in the Extensions tab (it is on Open VSX: https://open-vsx.org/extension/ehs-studio/dssl-vscode). In VS Code, download `dssl-vscode/dssl-vscode-0.12.1.vsix` from the repo files, then Extensions panel -> ... -> Install from VSIX. See `dssl-vscode/PUBLISHING.md` for publishing.
72
 
73
  AI agents and machine reviewers: this repo ships `llms.txt` (dense, structured facts), `AGENTS.md` (how to work on the codebase) and `PARITY.md` (the honest feature matrix against CSS). The site serves the same `llms.txt` at https://dssl-sable.vercel.app/llms.txt
74
 
dssl-vscode/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
  # Changelog
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  ## 0.12.0
4
 
5
  - Bundled engine updated to 0.12.0: `texture: ... symmetrical`, a `fonts { }` block, richer `gradient:` sentences
 
1
  # Changelog
2
 
3
+ ## 0.12.1
4
+
5
+ Found by running the extension inside a real VSCodium and clicking through it (`node scripts/test-editor-ui.mjs`):
6
+
7
+ - **Completions in a one-line rule.** `.t { texture: |` offered selectors and pseudo-elements instead of texture
8
+ words, because only a declaration that started its line was recognised. A declaration after `{` or `;` now
9
+ completes too.
10
+ - **The right words come first.** After `texture:` the presets and settings now sort above the long list of generic
11
+ CSS keywords, so `oak` is not buried under a hundred vendor keywords.
12
+ - **The designer knows wood.** The Texture tab now has `figure`, `warp`, `pores`, `knots` and `planks` (shown only for
13
+ the rings generator and the wood species) and a `mirror tile` choice for `symmetrical`. Before, these were written
14
+ and read back wrongly, and could not be tuned at all.
15
+
16
  ## 0.12.0
17
 
18
  - Bundled engine updated to 0.12.0: `texture: ... symmetrical`, a `fonts { }` block, richer `gradient:` sentences
dssl-vscode/extension.js CHANGED
@@ -71,6 +71,7 @@ function registerCompletion() {
71
  return result.items.map((it) => {
72
  const item = new vscode.CompletionItem(it.label, KIND[it.kind] || vscode.CompletionItemKind.Text);
73
  item.detail = it.detail;
 
74
  if (it.documentation) item.documentation = new vscode.MarkdownString(it.documentation);
75
  return item;
76
  });
 
71
  return result.items.map((it) => {
72
  const item = new vscode.CompletionItem(it.label, KIND[it.kind] || vscode.CompletionItemKind.Text);
73
  item.detail = it.detail;
74
+ if (it.sort) item.sortText = it.sort + it.label;
75
  if (it.documentation) item.documentation = new vscode.MarkdownString(it.documentation);
76
  return item;
77
  });
dssl-vscode/lib/analysis.js CHANGED
@@ -164,7 +164,11 @@ export function contextAt(text, offset) {
164
  const tokenDot = lineToCursor.match(/([\w-]+)\.([\w-]*)$/);
165
  // declaration line: "prop: value..." anchored at line start, but a line
166
  // that also opens a block ("a:hover {") is a selector, not a declaration
167
- let declMatch = lineToCursor.match(/^(\s*)(-{0,2}[.\w-]+)\s*:\s*([\s\S]*)$/);
 
 
 
 
168
  if (declMatch && declMatch[3].includes('{')) declMatch = null;
169
  // the value prefix is the trailing word segment: after "gap " (typed +
170
  // space) it is empty, so completions re-open for the next word
@@ -222,12 +226,16 @@ export function completionsAt(analysis, path, text, offset) {
222
  items.push({ label: k, kind: 'keyword', detail: 'animate option', documentation: ANIMATE_EXTRAS[k] || 'animate option' });
223
  }
224
  }
225
- items.push(...domainItems(prop));
226
- for (const k of CSS_KEYWORDS) {
227
- items.push({ label: k, kind: 'keyword', detail: 'keyword' });
228
- }
 
229
  for (const key of Object.keys(analysis.registry)) {
230
- items.push({ label: key, kind: 'value', detail: 'token = ' + analysis.registry[key], documentation: `token ${key} = ${analysis.registry[key]}` });
 
 
 
231
  }
232
  return { items, context: 'value' };
233
  }
 
164
  const tokenDot = lineToCursor.match(/([\w-]+)\.([\w-]*)$/);
165
  // declaration line: "prop: value..." anchored at line start, but a line
166
  // that also opens a block ("a:hover {") is a selector, not a declaration
167
+ // 0.12.0: the declaration is the part after the last "{", ";" or "}" on the line, so `.t { texture: |` and
168
+ // `.a { color: red; background: |` complete values too. Before this only a declaration that started its line did.
169
+ const cut = Math.max(lineToCursor.lastIndexOf('{'), lineToCursor.lastIndexOf(';'), lineToCursor.lastIndexOf('}'));
170
+ const statement = cut >= 0 ? lineToCursor.slice(cut + 1) : lineToCursor;
171
+ let declMatch = statement.match(/^(\s*)(-{0,2}[.\w-]+)\s*:\s*([\s\S]*)$/);
172
  if (declMatch && declMatch[3].includes('{')) declMatch = null;
173
  // the value prefix is the trailing word segment: after "gap " (typed +
174
  // space) it is empty, so completions re-open for the next word
 
226
  items.push({ label: k, kind: 'keyword', detail: 'animate option', documentation: ANIMATE_EXTRAS[k] || 'animate option' });
227
  }
228
  }
229
+ // the words that belong to THIS property (presets after texture:, settings after glass:) sort first, then tokens, then the long
230
+ // list of generic CSS keywords; otherwise the editor buries `oak` under a hundred vendor keywords
231
+ const mark = (list, sort) => { for (const it of list) it.sort = sort; return list; };
232
+ mark(items, '0');
233
+ items.push(...mark(domainItems(prop), '0'));
234
  for (const key of Object.keys(analysis.registry)) {
235
+ items.push({ label: key, kind: 'value', detail: 'token = ' + analysis.registry[key], documentation: `token ${key} = ${analysis.registry[key]}`, sort: '1' });
236
+ }
237
+ for (const k of CSS_KEYWORDS) {
238
+ items.push({ label: k, kind: 'keyword', detail: 'keyword', sort: '2' });
239
  }
240
  return { items, context: 'value' };
241
  }
dssl-vscode/media/designer-core.js CHANGED
@@ -155,7 +155,9 @@ export function auraFromDssl(text) {
155
  export const defaultTextureLayer = (name = 'grain') => ({ name, set: {}, color: null, color2: null });
156
  export const defaultTextureState = () => ({ layers: [defaultTextureLayer('wood')], base: '#b8a58a' });
157
 
158
- const TEXTURE_NUMERIC = ['scale', 'intensity', 'contrast', 'threshold', 'octaves', 'seed', 'depth', 'light', 'angle', 'thickness', 'radius', 'softness', 'contour', 'animate'];
 
 
159
 
160
  /** what a preset or generator would use with no overrides, to know what to leave out of the output */
161
  function baselineFor(name) {
@@ -170,6 +172,7 @@ export function textureLayerToDssl(layer) {
170
  if (layer.set.stretch && (layer.set.stretch[0] !== (base.stretch || [1, 1])[0] || layer.set.stretch[1] !== (base.stretch || [1, 1])[1])) w.push('stretch', fmt(layer.set.stretch[0]), fmt(layer.set.stretch[1]));
171
  if (layer.set.type && layer.set.type !== base.type) w.push('type', layer.set.type);
172
  if (layer.set.blend && layer.set.blend !== base.blend) w.push('blend', layer.set.blend);
 
173
  if (layer.set.invert) w.push('invert');
174
  if (layer.color) w.push('color', colorToDssl(layer.color));
175
  if (layer.color2) w.push('color2', colorToDssl(layer.color2));
@@ -181,7 +184,7 @@ export const textureToDssl = (state) => (state.layers.length ? `texture: ${state
181
  function fromParsedTexture(parsed) {
182
  const layer = defaultTextureLayer(parsed.preset || parsed.gen);
183
  const base = baselineFor(layer.name);
184
- for (const k of [...TEXTURE_NUMERIC, 'stretch', 'type', 'blend', 'invert']) if (parsed[k] !== undefined && JSON.stringify(parsed[k]) !== JSON.stringify(base[k])) layer.set[k] = parsed[k];
185
  if (parsed.color !== undefined && parsed.color !== base.color) layer.color = dsslToColor(parsed.color);
186
  if (parsed.color2 !== undefined && parsed.color2 !== base.color2) layer.color2 = dsslToColor(parsed.color2);
187
  return layer;
 
155
  export const defaultTextureLayer = (name = 'grain') => ({ name, set: {}, color: null, color2: null });
156
  export const defaultTextureState = () => ({ layers: [defaultTextureLayer('wood')], base: '#b8a58a' });
157
 
158
+ const TEXTURE_NUMERIC = ['scale', 'intensity', 'contrast', 'threshold', 'octaves', 'seed', 'depth', 'light', 'angle', 'thickness', 'radius', 'softness', 'contour', 'animate', 'figure', 'warp', 'pores', 'knots', 'planks'];
159
+ /** the settings that only mean something for the rings (wood) generator */
160
+ export const RINGS_ONLY = ['figure', 'warp', 'pores', 'knots', 'planks'];
161
 
162
  /** what a preset or generator would use with no overrides, to know what to leave out of the output */
163
  function baselineFor(name) {
 
172
  if (layer.set.stretch && (layer.set.stretch[0] !== (base.stretch || [1, 1])[0] || layer.set.stretch[1] !== (base.stretch || [1, 1])[1])) w.push('stretch', fmt(layer.set.stretch[0]), fmt(layer.set.stretch[1]));
173
  if (layer.set.type && layer.set.type !== base.type) w.push('type', layer.set.type);
174
  if (layer.set.blend && layer.set.blend !== base.blend) w.push('blend', layer.set.blend);
175
+ if (layer.set.symmetrical && layer.set.symmetrical !== base.symmetrical) w.push('symmetrical', ...(layer.set.symmetrical === 'both' ? [] : [layer.set.symmetrical]));
176
  if (layer.set.invert) w.push('invert');
177
  if (layer.color) w.push('color', colorToDssl(layer.color));
178
  if (layer.color2) w.push('color2', colorToDssl(layer.color2));
 
184
  function fromParsedTexture(parsed) {
185
  const layer = defaultTextureLayer(parsed.preset || parsed.gen);
186
  const base = baselineFor(layer.name);
187
+ for (const k of [...TEXTURE_NUMERIC, 'stretch', 'type', 'blend', 'invert', 'symmetrical']) if (parsed[k] !== undefined && JSON.stringify(parsed[k]) !== JSON.stringify(base[k])) layer.set[k] = parsed[k];
188
  if (parsed.color !== undefined && parsed.color !== base.color) layer.color = dsslToColor(parsed.color);
189
  if (parsed.color2 !== undefined && parsed.color2 !== base.color2) layer.color2 = dsslToColor(parsed.color2);
190
  return layer;
dssl-vscode/media/designer.js CHANGED
@@ -279,7 +279,7 @@ function buildTextureTab() {
279
  const settings = h('div', {});
280
  const swatch = h('div', { class: 'swatch', id: 'tex-swatch' });
281
  const baseColor = h('input', { type: 'color', value: s.base, onchange: (e) => { s.base = e.target.value; swatch.style.backgroundColor = s.base; } });
282
- const numeric = ['scale', 'intensity', 'contrast', 'threshold', 'octaves', 'seed', 'depth', 'light', 'angle', 'thickness', 'radius', 'softness', 'contour', 'animate'];
283
 
284
  function rebuild() {
285
  layerBar.replaceChildren(...s.layers.map((l, i) => h('button', { class: 'mini' + (i === tex.sel ? ' on' : ''), onclick: () => { tex.sel = i; rebuild(); } }, `${i + 1}. ${l.name}`)),
@@ -291,7 +291,9 @@ function buildTextureTab() {
291
  const eff = (k) => (L.set[k] !== undefined ? L.set[k] : base[k]);
292
  const info = h('div', { class: 'note' }, PRESETS[L.name] ? `${L.name}: ${PRESETS[L.name].doc}` : `${L.name} (a generator: set it up yourself)`);
293
  settings.append(info);
 
294
  for (const k of numeric) {
 
295
  const def = TEXTURE_KEYS[k];
296
  const dflt = base[k] !== undefined ? base[k] : (def.min + Math.min(def.max, 20) / 2);
297
  const step = def.int ? 1 : def.max <= 2 ? 0.05 : def.max <= 40 ? 0.1 : 1;
@@ -304,6 +306,7 @@ function buildTextureTab() {
304
  num('stretch y', () => (L.set.stretch || st)[1], (v) => { L.set.stretch = [(L.set.stretch || st)[0], v]; }, 1, 80, 1),
305
  select('blend', () => eff('blend') || 'normal', (v) => { L.set.blend = v; }, ['normal', 'multiply', 'screen', 'overlay', 'soft-light', 'hard-light', 'color-burn', 'color-dodge', 'darken', 'lighten', 'difference', 'exclusion']),
306
  select('type', () => eff('type') || 'fractal', (v) => { L.set.type = v; }, ['fractal', 'turbulence']),
 
307
  check('invert', () => !!L.set.invert, (v) => { L.set.invert = v || undefined; }),
308
  h('div', { class: 'pair' },
309
  check('own colour', () => !!L.color, (v) => { L.color = v ? (L.color || { hex: '#000000', a: 1 }) : null; rebuild(); }),
 
279
  const settings = h('div', {});
280
  const swatch = h('div', { class: 'swatch', id: 'tex-swatch' });
281
  const baseColor = h('input', { type: 'color', value: s.base, onchange: (e) => { s.base = e.target.value; swatch.style.backgroundColor = s.base; } });
282
+ const numeric = ['scale', 'intensity', 'contrast', 'threshold', 'octaves', 'seed', 'depth', 'light', 'angle', 'thickness', 'radius', 'softness', 'contour', 'animate', ...core.RINGS_ONLY];
283
 
284
  function rebuild() {
285
  layerBar.replaceChildren(...s.layers.map((l, i) => h('button', { class: 'mini' + (i === tex.sel ? ' on' : ''), onclick: () => { tex.sel = i; rebuild(); } }, `${i + 1}. ${l.name}`)),
 
291
  const eff = (k) => (L.set[k] !== undefined ? L.set[k] : base[k]);
292
  const info = h('div', { class: 'note' }, PRESETS[L.name] ? `${L.name}: ${PRESETS[L.name].doc}` : `${L.name} (a generator: set it up yourself)`);
293
  settings.append(info);
294
+ const isRings = (base.gen || L.name) === 'rings';
295
  for (const k of numeric) {
296
+ if (core.RINGS_ONLY.includes(k) && !isRings) continue; // wood settings only show for wood
297
  const def = TEXTURE_KEYS[k];
298
  const dflt = base[k] !== undefined ? base[k] : (def.min + Math.min(def.max, 20) / 2);
299
  const step = def.int ? 1 : def.max <= 2 ? 0.05 : def.max <= 40 ? 0.1 : 1;
 
306
  num('stretch y', () => (L.set.stretch || st)[1], (v) => { L.set.stretch = [(L.set.stretch || st)[0], v]; }, 1, 80, 1),
307
  select('blend', () => eff('blend') || 'normal', (v) => { L.set.blend = v; }, ['normal', 'multiply', 'screen', 'overlay', 'soft-light', 'hard-light', 'color-burn', 'color-dodge', 'darken', 'lighten', 'difference', 'exclusion']),
308
  select('type', () => eff('type') || 'fractal', (v) => { L.set.type = v; }, ['fractal', 'turbulence']),
309
+ select('mirror tile (symmetrical)', () => L.set.symmetrical || 'none', (v) => { L.set.symmetrical = v === 'none' ? undefined : v; }, ['none', 'both', 'x', 'y']),
310
  check('invert', () => !!L.set.invert, (v) => { L.set.invert = v || undefined; }),
311
  h('div', { class: 'pair' },
312
  check('own colour', () => !!L.color, (v) => { L.color = v ? (L.color || { hex: '#000000', a: 1 }) : null; rebuild(); }),
dssl-vscode/package.json CHANGED
@@ -2,7 +2,7 @@
2
  "name": "dssl-vscode",
3
  "displayName": "DSSL Language Support",
4
  "description": "The DSSL style language in your editor: syntax highlighting, real-compiler diagnostics (the same errors the browser console shows), completions from the live token registry and for textures, auras and sliders, hover docs, an interactive designer with sliders and live previews for aura, texture and range sliders, and a CSS to DSSL converter. Works in VS Code and VSCodium.",
5
- "version": "0.12.0",
6
  "publisher": "ehs-studio",
7
  "icon": "icon.png",
8
  "license": "MIT",
 
2
  "name": "dssl-vscode",
3
  "displayName": "DSSL Language Support",
4
  "description": "The DSSL style language in your editor: syntax highlighting, real-compiler diagnostics (the same errors the browser console shows), completions from the live token registry and for textures, auras and sliders, hover docs, an interactive designer with sliders and live previews for aura, texture and range sliders, and a CSS to DSSL converter. Works in VS Code and VSCodium.",
5
+ "version": "0.12.1",
6
  "publisher": "ehs-studio",
7
  "icon": "icon.png",
8
  "license": "MIT",
llms.txt CHANGED
@@ -90,4 +90,4 @@ Wait for the `dssl-ready` event on `window` before measuring layout: the engine
90
 
91
  ## Editor support
92
 
93
- The official extension (VS Code and VSCodium, same .vsix) bundles the real parser and resolver: diagnostics identical to the browser console, completions from the workspace token registry, hover docs for the semantic vocabulary. In VSCodium, search "DSSL" in the Extensions tab (it is on Open VSX: https://open-vsx.org/extension/ehs-studio/dssl-vscode). In VS Code, download `dssl-vscode/dssl-vscode-0.12.0.vsix` from the repo files, then Extensions panel -> ... -> Install from VSIX. Source lives in `dssl-vscode/` in the repo.
 
90
 
91
  ## Editor support
92
 
93
+ The official extension (VS Code and VSCodium, same .vsix) bundles the real parser and resolver: diagnostics identical to the browser console, completions from the workspace token registry, hover docs for the semantic vocabulary. In VSCodium, search "DSSL" in the Extensions tab (it is on Open VSX: https://open-vsx.org/extension/ehs-studio/dssl-vscode). In VS Code, download `dssl-vscode/dssl-vscode-0.12.1.vsix` from the repo files, then Extensions panel -> ... -> Install from VSIX. Source lives in `dssl-vscode/` in the repo.
public/llms.txt CHANGED
@@ -90,4 +90,4 @@ Wait for the `dssl-ready` event on `window` before measuring layout: the engine
90
 
91
  ## Editor support
92
 
93
- The official extension (VS Code and VSCodium, same .vsix) bundles the real parser and resolver: diagnostics identical to the browser console, completions from the workspace token registry, hover docs for the semantic vocabulary. In VSCodium, search "DSSL" in the Extensions tab (it is on Open VSX: https://open-vsx.org/extension/ehs-studio/dssl-vscode). In VS Code, download `dssl-vscode/dssl-vscode-0.12.0.vsix` from the repo files, then Extensions panel -> ... -> Install from VSIX. Source lives in `dssl-vscode/` in the repo.
 
90
 
91
  ## Editor support
92
 
93
+ The official extension (VS Code and VSCodium, same .vsix) bundles the real parser and resolver: diagnostics identical to the browser console, completions from the workspace token registry, hover docs for the semantic vocabulary. In VSCodium, search "DSSL" in the Extensions tab (it is on Open VSX: https://open-vsx.org/extension/ehs-studio/dssl-vscode). In VS Code, download `dssl-vscode/dssl-vscode-0.12.1.vsix` from the repo files, then Extensions panel -> ... -> Install from VSIX. Source lives in `dssl-vscode/` in the repo.
scripts/lib/editor.mjs ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* editor.mjs: find a VS Code or VSCodium to run the extension tests in, and launch it with a throwaway profile. */
2
+ import { spawn, spawnSync } from 'node:child_process';
3
+ import { cpSync, mkdirSync, rmSync } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ export const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
8
+ export const ext = path.join(root, 'dssl-vscode');
9
+ const which = (cmd) => spawnSync('sh', ['-c', `command -v ${cmd}`], { encoding: 'utf8' }).stdout.trim();
10
+
11
+ /** { cmd, pre, kill } or null. pre = arguments that go before the editor's own (flatpak needs them) */
12
+ export function findEditor(extraEnv = {}) {
13
+ if (process.env.DSSL_EDITOR) return { cmd: process.env.DSSL_EDITOR, pre: [], kill: () => {} };
14
+ for (const c of ['codium', 'code']) if (which(c)) return { cmd: c, pre: [], kill: () => {} };
15
+ if (which('flatpak') && /com\.vscodium\.codium/.test(spawnSync('flatpak', ['list', '--app'], { encoding: 'utf8' }).stdout)) {
16
+ const envs = Object.entries(extraEnv).map(([k, v]) => `--env=${k}=${v}`);
17
+ return { cmd: 'flatpak', pre: ['run', ...envs, 'com.vscodium.codium'], kill: () => spawnSync('flatpak', ['kill', 'com.vscodium.codium']) };
18
+ }
19
+ return null;
20
+ }
21
+
22
+ /** a clean scratch profile + workspace copy of the fixtures, so the editor never touches your own settings */
23
+ export function scratchWorkspace(name) {
24
+ const dir = path.join(root, '.vsc-test', name);
25
+ rmSync(dir, { recursive: true, force: true });
26
+ mkdirSync(path.join(dir, 'ws'), { recursive: true });
27
+ cpSync(path.join(ext, 'test/host/fixtures'), path.join(dir, 'ws'), { recursive: true });
28
+ return { dir, ws: path.join(dir, 'ws') };
29
+ }
30
+
31
+ export function editorArgs(editor, dir, ws, more = []) {
32
+ return [...editor.pre, `--user-data-dir=${path.join(dir, 'user')}`, `--extensions-dir=${path.join(dir, 'extensions')}`,
33
+ `--extensionDevelopmentPath=${ext}`, '--disable-workspace-trust', '--new-window', '--disable-gpu', ...more, ws, path.join(ws, 'sample.dssl')];
34
+ }
35
+
36
+ export const launchDetached = (editor, args, env = {}) => spawn(editor.cmd, args, { stdio: 'ignore', detached: true, env: { ...process.env, ...env } });
scripts/test-all.mjs CHANGED
@@ -10,6 +10,7 @@ import { findChrome } from './lib/browser-harness.mjs';
10
 
11
  const here = dirname(fileURLToPath(import.meta.url));
12
  const skipBrowser = process.argv.includes('--no-browser');
 
13
  const SUITES = [
14
  ['engine 0.5', 'test-v050.mjs'], ['engine 0.6', 'test-v060.mjs'], ['engine 0.7', 'test-v070.mjs'], ['parser lines 0.7.1', 'test-v071.mjs'],
15
  ['review fixes 0.7.2', 'test-v072.mjs'], ['exit lane 0.8.0', 'test-v073.mjs'], ['follow-up 0.8.1', 'test-v081.mjs'], ['cascade 0.8.2', 'test-v082.mjs'],
@@ -24,6 +25,7 @@ if (!skipBrowser && !findChrome()) {
24
  process.exit(2);
25
  }
26
  const rows = [];
 
27
  for (const [name, file, browser] of SUITES) {
28
  if (browser && skipBrowser) { rows.push([name, 'skipped', '']); continue; }
29
  const t0 = Date.now();
 
10
 
11
  const here = dirname(fileURLToPath(import.meta.url));
12
  const skipBrowser = process.argv.includes('--no-browser');
13
+ const withEditor = process.argv.includes('--editor'); // also run the extension inside a real VS Code / VSCodium (needs a display)
14
  const SUITES = [
15
  ['engine 0.5', 'test-v050.mjs'], ['engine 0.6', 'test-v060.mjs'], ['engine 0.7', 'test-v070.mjs'], ['parser lines 0.7.1', 'test-v071.mjs'],
16
  ['review fixes 0.7.2', 'test-v072.mjs'], ['exit lane 0.8.0', 'test-v073.mjs'], ['follow-up 0.8.1', 'test-v081.mjs'], ['cascade 0.8.2', 'test-v082.mjs'],
 
25
  process.exit(2);
26
  }
27
  const rows = [];
28
+ if (withEditor) SUITES.push(['extension inside a real editor', 'test-in-editor.mjs', true], ['designer and typing in a real editor', 'test-editor-ui.mjs', true]);
29
  for (const [name, file, browser] of SUITES) {
30
  if (browser && skipBrowser) { rows.push([name, 'skipped', '']); continue; }
31
  const t0 = Date.now();
scripts/test-designer.mjs CHANGED
@@ -121,5 +121,18 @@ section('random sweep: everything the panel can write compiles and reads back');
121
  ok(`${n} random designs all compile and read back (aura both styles, texture, slider)`, bad.length === 0, `${bad.length} problems; first: ${bad[0]}`);
122
  }
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  console.log(`\n${pass} passed, ${fail} failed`);
125
  process.exit(fail ? 1 : 0);
 
121
  ok(`${n} random designs all compile and read back (aura both styles, texture, slider)`, bad.length === 0, `${bad.length} problems; first: ${bad[0]}`);
122
  }
123
 
124
+ // 0.11/0.12: the wood settings and the mirrored tile survive the designer both ways
125
+ {
126
+ const mk = (set) => ({ layers: [{ name: 'oak', set, color: null, color2: null }], base: '#b8a58a' });
127
+ const t1 = core.textureToDssl(mk({ planks: 120, knots: 2, figure: 0.8, symmetrical: 'both' }));
128
+ ok('wood settings are written (planks, knots, figure, symmetrical)', /^texture: oak /.test(t1) && /planks 120/.test(t1) && /knots 2/.test(t1) && /figure 0.8/.test(t1) && /symmetrical(?! (x|y|both))/.test(t1), t1);
129
+ ok('and the result compiles', compiles(t1).ok, JSON.stringify(compiles(t1)));
130
+ const back = core.textureFromDssl(t1).state.layers[0];
131
+ ok('and reads back into the same controls', back.name === 'oak' && back.set.planks === 120 && back.set.knots === 2 && back.set.figure === 0.8 && back.set.symmetrical === 'both', JSON.stringify(back));
132
+ ok('x and y are kept', /symmetrical x/.test(core.textureToDssl(mk({ symmetrical: 'x' }))) && core.textureFromDssl('texture: oak symmetrical y').state.layers[0].set.symmetrical === 'y');
133
+ ok('a preset that already says nothing special writes just its name', core.textureToDssl(mk({})) === 'texture: oak');
134
+ ok('RINGS_ONLY lists exactly the settings that only mean something for wood', JSON.stringify(core.RINGS_ONLY) === JSON.stringify(['figure', 'warp', 'pores', 'knots', 'planks']));
135
+ }
136
+
137
  console.log(`\n${pass} passed, ${fail} failed`);
138
  process.exit(fail ? 1 : 0);
scripts/test-editor-ui.mjs ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+ /* test-editor-ui.mjs: drive a REAL VS Code or VSCodium like a person would.
3
+ *
4
+ * node scripts/test-editor-ui.mjs
5
+ *
6
+ * Launches the editor with the extension loaded and a debugging port, then uses Playwright over the
7
+ * Chrome DevTools Protocol (the editor is Electron) to type, hover, press F1, click inside the designer
8
+ * webview, and read the result back out of the open file. It needs a display and an editor, so it is not part
9
+ * of test-all.mjs: run it by hand before you publish the extension, together with test-in-editor.mjs. */
10
+ import { chromium } from 'playwright-core';
11
+ import { readFileSync } from 'node:fs';
12
+ import { findEditor, scratchWorkspace, editorArgs, launchDetached } from './lib/editor.mjs';
13
+
14
+ const PORT = 9400 + Math.floor(Math.random() * 400);
15
+ const editor = findEditor();
16
+ if (!editor) { console.error('No VS Code or VSCodium found. Install one, or set DSSL_EDITOR to its command.'); process.exit(2); }
17
+ const { dir, ws } = scratchWorkspace('ui');
18
+ const child = launchDetached(editor, editorArgs(editor, dir, ws, [`--remote-debugging-port=${PORT}`]));
19
+ child.unref();
20
+
21
+ let pass = 0, fail = 0;
22
+ const ok = (n, c, x) => { if (c) { pass++; console.log(` ok ${n}`); } else { fail++; console.log(`FAIL ${n}${x !== undefined ? '; ' + x : ''}`); } };
23
+ const done = async (browser, code) => { try { await browser.close(); } catch { /* already gone */ } try { editor.kill(); child.kill(); } catch { /* ignore */ } console.log(`\n${pass} passed, ${fail} failed (clicking through a real editor)`); process.exit(code); };
24
+
25
+ let browser = null;
26
+ for (let i = 0; i < 60 && !browser; i++) {
27
+ try { browser = await chromium.connectOverCDP(`http://127.0.0.1:${PORT}`); } catch { await new Promise((r) => setTimeout(r, 1000)); }
28
+ }
29
+ if (!browser) { console.error('could not reach the editor'); editor.kill(); process.exit(1); }
30
+ const page = browser.contexts()[0].pages()[0];
31
+ await page.waitForTimeout(4500);
32
+ await page.click('.notification-toast .monaco-button:has-text("Never")').catch(() => {});
33
+ const editorText = () => page.evaluate(() => [...document.querySelectorAll('.monaco-editor .view-line')].map((l) => l.textContent.replace(/ /g, ' ')).join('\n'));
34
+ const webview = () => page.frames().filter((x) => /vscode-webview/.test(x.url())).pop();
35
+ const palette = async (text) => { await page.keyboard.press('F1'); await page.waitForTimeout(500); await page.keyboard.type(text); await page.waitForTimeout(700); await page.keyboard.press('Enter'); await page.waitForTimeout(3500); };
36
+
37
+ try {
38
+ // completions in a one-line rule, the case a real editor found broken
39
+ await page.click('.monaco-editor .view-lines'); await page.keyboard.press('Control+End');
40
+ await page.keyboard.type('\n.t { texture: ');
41
+ await page.keyboard.press('Control+Space'); await page.waitForTimeout(1500);
42
+ const list1 = await page.locator('.suggest-widget .monaco-list-row').allTextContents();
43
+ ok('one-line rule: "texture: " offers texture words first, not pseudo-elements', list1.length > 3 && list1.slice(0, 8).every((t) => /texture (preset|setting|generator)|blend mode/.test(t)) && !list1.some((t) => /::after/.test(t)), list1.slice(0, 6).join(' | '));
44
+ await page.keyboard.type('oa'); await page.waitForTimeout(700);
45
+ const list2 = await page.locator('.suggest-widget .monaco-list-row').allTextContents();
46
+ ok('typing "oa" narrows it to oak and oak-planks', list2.some((t) => /^oak\b/.test(t)) && list2.some((t) => /oak-planks/.test(t)), list2.join(' | '));
47
+ await page.keyboard.press('Escape');
48
+ await page.keyboard.press('Control+Shift+K'); await page.waitForTimeout(300); // delete the scratch line
49
+
50
+ // hover
51
+ await page.mouse.move(470, 348); await page.waitForTimeout(1800);
52
+ const hover = await page.locator('.monaco-hover').allTextContents();
53
+ ok('hovering "texture" shows the documentation widget', hover.some((t) => /generated texture/i.test(t)), hover.join('|').slice(0, 120));
54
+ await page.mouse.move(900, 700); await page.waitForTimeout(300);
55
+
56
+ // problems: the status bar counts what the squiggles say
57
+ const status = (await page.locator('.statusbar').innerText()).replace(/\s+/g, ' ');
58
+ ok('the status bar counts the errors and the warning', /[3-9]/.test(status) && /1/.test(status), status.slice(0, 60));
59
+
60
+ // the designer
61
+ await palette('DSSL: Design a texture');
62
+ let f = webview();
63
+ ok('"DSSL: Design a texture" opens the designer on the Texture tab', f && /Textures/.test(await f.locator('body').innerText()));
64
+ const tile = async (name) => { const t = webview().locator(`.tile[data-name="${name}"]`); await t.scrollIntoViewIfNeeded(); await t.click(); await page.waitForTimeout(500); };
65
+ await tile('oak-planks');
66
+ const txt = await webview().locator('body').innerText();
67
+ ok('wood settings appear for a wood preset (planks, knots, figure, mirror tile)', ['planks', 'knots', 'figure', 'mirror tile'].every((w) => txt.includes(w)));
68
+ await tile('grain');
69
+ ok('and are hidden for grain', !(await webview().locator('body').innerText()).includes('knots'));
70
+ await tile('oak-planks');
71
+ await webview().locator('select').filter({ has: webview().locator('option[value="both"]') }).first().selectOption('both'); await page.waitForTimeout(500);
72
+ const dsslOut = await webview().locator('textarea').last().inputValue();
73
+ ok('choosing the mirror writes symmetrical into the DSSL', /^texture: oak-planks symmetrical$/.test(dsslOut.trim()), dsslOut);
74
+ ok('and the panel says it compiles', /compiles/.test(await webview().locator('body').innerText()));
75
+
76
+ // insert, then load it back
77
+ await page.click('.monaco-editor .view-lines'); await page.keyboard.press('Control+End');
78
+ await webview().getByRole('button', { name: 'Insert at cursor' }).click(); await page.waitForTimeout(800);
79
+ ok('"Insert at cursor" puts the line into the open file', /texture: oak-planks symmetrical/.test(await editorText()));
80
+ await page.click('.monaco-editor .view-lines'); await page.keyboard.press('Control+End'); await page.keyboard.press('Shift+Home'); await page.waitForTimeout(200);
81
+ await tile('grain');
82
+ await webview().getByRole('button', { name: 'Load from selection' }).click(); await page.waitForTimeout(800);
83
+ ok('"Load from selection" reads the line back into the controls', /oak-planks symmetrical/.test(await webview().locator('textarea').last().inputValue()));
84
+
85
+ // the other tabs
86
+ await webview().getByRole('button', { name: 'Slider' }).first().click(); await page.waitForTimeout(800);
87
+ ok('the slider tab shows a live preview slider that compiles', (await webview().locator('#pv').count()) === 1 && /compiles/.test(await webview().locator('body').innerText()));
88
+ await webview().getByRole('button', { name: 'Aura' }).first().click(); await page.waitForTimeout(800);
89
+ ok('the aura tab compiles too', /compiles/.test(await webview().locator('body').innerText()));
90
+ await done(browser, fail ? 1 : 0);
91
+ } catch (e) {
92
+ fail++; console.log('FAIL the run threw: ' + (e && e.message ? e.message.split('\n')[0] : e));
93
+ await page.screenshot({ path: dir + '/failure.png' }).catch(() => {});
94
+ console.log(' a screenshot of the editor is at ' + dir + '/failure.png');
95
+ await done(browser, 1);
96
+ }
scripts/test-extension-lib.mjs CHANGED
@@ -4,7 +4,7 @@
4
  * behave through the exact code the .vsix ships. Run before packaging. */
5
  import { resolve } from '../dssl-vscode/lib/engine/resolver.js';
6
  import { parse, STATE_WORDS } from '../dssl-vscode/lib/engine/parser.js';
7
- import { analyzeWorkspace, diagnosticsFor } from '../dssl-vscode/lib/analysis.js';
8
 
9
  let pass = 0, fail = 0;
10
  const ok = (name, cond) => { if (cond) { pass++; console.log(` ok ${name}`); } else { fail++; console.log(`FAIL ${name}`); } };
@@ -40,5 +40,22 @@ ok('STATE_WORDS exported by bundled parser', STATE_WORDS instanceof Set && STATE
40
  const tDoc = compileDoc('card { transition-delay: 200 }', 't.dssl');
41
  ok('bare transition-delay becomes 200ms', /200\s*ms|200ms/.test(JSON.stringify(tDoc.rules[0].decls)));
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  console.log(`\n${pass} passed, ${fail} failed`);
44
  process.exit(fail ? 1 : 0);
 
4
  * behave through the exact code the .vsix ships. Run before packaging. */
5
  import { resolve } from '../dssl-vscode/lib/engine/resolver.js';
6
  import { parse, STATE_WORDS } from '../dssl-vscode/lib/engine/parser.js';
7
+ import { analyzeWorkspace, diagnosticsFor, completionsAt } from '../dssl-vscode/lib/analysis.js';
8
 
9
  let pass = 0, fail = 0;
10
  const ok = (name, cond) => { if (cond) { pass++; console.log(` ok ${name}`); } else { fail++; console.log(`FAIL ${name}`); } };
 
40
  const tDoc = compileDoc('card { transition-delay: 200 }', 't.dssl');
41
  ok('bare transition-delay becomes 200ms', /200\s*ms|200ms/.test(JSON.stringify(tDoc.rules[0].decls)));
42
 
43
+ // completions where the declaration does not start the line (found by driving a real editor, 0.12.0)
44
+ {
45
+ const an = analyzeWorkspace(new Map([['/w/a.dssl', '.a { color: red }']]));
46
+ const labels = (text) => completionsAt(an, '/w/a.dssl', text, text.length).items.map((x) => x.label);
47
+ const oneLine = labels('.t { texture: ');
48
+ ok('one-line rule: after "texture: " the presets are offered', oneLine.includes('oak') && oneLine.includes('rings') && !oneLine.includes('::after'), oneLine.slice(0, 5).join());
49
+ const second = labels('.t { color: red; glass: ');
50
+ ok('the words for the property sort before generic CSS keywords', (() => { const it = completionsAt(an, '/w/a.dssl', '.t { texture: ', 14).items; const oak = it.find((x) => x.label === 'oak'), kw = it.find((x) => x.label === 'absolute'); return oak && kw && (oak.sort + oak.label) < (kw.sort + kw.label); })());
51
+ ok('a second declaration on the line completes too', second.includes('saturate') && second.includes('tint'), second.slice(0, 5).join());
52
+ const afterBrace = labels('.t { tilt: ');
53
+ ok('tilt settings after the brace', afterBrace.includes('lift') && afterBrace.includes('glare'));
54
+ const fresh = labels('.t { ');
55
+ ok('right after "{" it still offers property names, not values', fresh.includes('background') && !fresh.includes('oak'));
56
+ ok('a normal multi-line declaration still works', labels('.t {\n texture: ').includes('oak'));
57
+ ok('a nested selector line is still not a declaration', !labels('.t {\n a:hover').includes('oak'));
58
+ }
59
+
60
  console.log(`\n${pass} passed, ${fail} failed`);
61
  process.exit(fail ? 1 : 0);
scripts/test-in-editor.mjs ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+ /* test-in-editor.mjs: run the extension inside a REAL VS Code or VSCodium.
3
+ *
4
+ * node scripts/test-in-editor.mjs finds VSCodium (flatpak or on PATH) or VS Code
5
+ * DSSL_EDITOR="code" node scripts/test-in-editor.mjs
6
+ *
7
+ * It opens a real editor window (a throwaway profile, so yours is untouched), loads the extension
8
+ * from this checkout, runs dssl-vscode/test/host/index.js in the extension host, and prints the
9
+ * results. Needs a display, so it is not part of test-all.mjs.
10
+ * The UI half (typing, hover, the designer panel) is scripts/test-editor-ui.mjs. */
11
+ import { spawnSync } from 'node:child_process';
12
+ import { existsSync, readFileSync } from 'node:fs';
13
+ import path from 'node:path';
14
+ import { ext, findEditor, scratchWorkspace, editorArgs } from './lib/editor.mjs';
15
+
16
+ const { dir, ws } = scratchWorkspace('host');
17
+ const out = path.join(dir, 'results.json');
18
+ const editor = findEditor({ DSSL_TEST_OUT: out });
19
+ if (!editor) { console.error('No VS Code or VSCodium found. Install one, or set DSSL_EDITOR to its command.'); process.exit(2); }
20
+ console.log(`launching: ${editor.cmd} ${editor.pre.filter((a) => !a.startsWith('--env')).join(' ')} ...`);
21
+ const r = spawnSync(editor.cmd, editorArgs(editor, dir, ws, [`--extensionTestsPath=${path.join(ext, 'test/host')}`]), { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8', env: { ...process.env, DSSL_TEST_OUT: out }, timeout: 120000 });
22
+
23
+ if (!existsSync(out)) {
24
+ console.error('The editor exited without writing results.');
25
+ console.error((r.stdout || '').slice(-1500)); console.error((r.stderr || '').slice(-2500));
26
+ process.exit(1);
27
+ }
28
+ const results = JSON.parse(readFileSync(out, 'utf8'));
29
+ let pass = 0, fail = 0;
30
+ for (const x of results) { if (x.ok) { pass++; console.log(` ok ${x.name}`); } else { fail++; console.log(`FAIL ${x.name}; ${x.extra}`); } }
31
+ console.log(`\n${pass} passed, ${fail} failed (in a real editor)`);
32
+ process.exit(fail ? 1 : 0);