Commit ·
ee84700
1
Parent(s): 8181411
Extension 0.12.1: found and fixed by running it in a real VSCodium
Browse filesOne-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 +3 -0
- GUIDE.md +8 -1
- README.md +1 -1
- dssl-vscode/CHANGELOG.md +13 -0
- dssl-vscode/extension.js +1 -0
- dssl-vscode/lib/analysis.js +14 -6
- dssl-vscode/media/designer-core.js +5 -2
- dssl-vscode/media/designer.js +4 -1
- dssl-vscode/package.json +1 -1
- llms.txt +1 -1
- public/llms.txt +1 -1
- scripts/lib/editor.mjs +36 -0
- scripts/test-all.mjs +2 -0
- scripts/test-designer.mjs +13 -0
- scripts/test-editor-ui.mjs +96 -0
- scripts/test-extension-lib.mjs +18 -1
- scripts/test-in-editor.mjs +32 -0
.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.
|
| 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.
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 226 |
-
|
| 227 |
-
|
| 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.
|
| 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.
|
|
|
|
| 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.
|
|
|
|
| 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);
|