dssl / dssl-vscode /extension.js
EHS-Studio's picture
Claude Sonnet 5.5
Extension 0.12.1: found and fixed by running it in a real VSCodium
ee84700
Raw History Blame Contribute Delete
9.83 kB
/* extension.js - thin vscode wiring over lib/analysis.js.
* The analysis layer (and the DSSL compiler it wraps) is ESM; the extension
* host is happy with a dynamic import from CommonJS. Everything testable
* lives in lib/, this file only talks to the editor. */
const vscode = require('vscode');
let mod = null; // lib/analysis.js module
let analysis = null; // current workspace analysis result
let diags = null; // DiagnosticCollection (created once)
let debounceTimer = null;
let designerHost = null; // lib/designer-host.js
let designerPanel = null;
let lastDsslEditor = null; // the panel steals focus, so remember which .dssl editor to insert into
const KIND = {
property: vscode.CompletionItemKind.Property,
value: vscode.CompletionItemKind.Value,
keyword: vscode.CompletionItemKind.Keyword,
block: vscode.CompletionItemKind.Snippet,
};
function debounceMs() {
return vscode.workspace.getConfiguration('dssl').get('diagnostics.debounce', 250);
}
/* Fold the whole workspace: every .dssl file parsed once, tokens chained in
* docs-first order (tokens*.dssl before the rest, then alphabetical). */
async function runAnalysis() {
if (!mod) return;
const uris = await vscode.workspace.findFiles('**/*.dssl', '**/node_modules/**');
const docs = new Map();
await Promise.all(uris.map(async (uri) => {
try {
const doc = await vscode.workspace.openTextDocument(uri);
docs.set(uri.fsPath, doc.getText());
} catch { /* file vanished mid-scan */ }
}));
analysis = mod.analyzeWorkspace(docs);
const next = new Map();
for (const [p] of analysis.perFile) {
next.set(vscode.Uri.file(p), mod.diagnosticsFor(analysis, p).map((d) => {
const range = new vscode.Range(d.line, d.col, d.line, 400);
const sev = d.severity === 'error' ? vscode.DiagnosticSeverity.Error : vscode.DiagnosticSeverity.Warning;
const diag = new vscode.Diagnostic(range, d.message, sev);
diag.source = 'dssl';
return diag;
}));
}
// clear stale files (deleted from disk or emptied) then set fresh entries
diags.clear();
for (const [uri, items] of next) diags.set(uri, items);
}
function scheduleAnalysis() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => { runAnalysis().catch(() => {}); }, debounceMs());
}
function registerCompletion() {
return vscode.languages.registerCompletionItemProvider(
{ language: 'dssl' },
{
provideCompletionItems(document, position) {
if (!analysis || !mod) return [];
const text = document.getText();
const offset = document.offsetAt(position);
const result = mod.completionsAt(analysis, document.uri.fsPath, text, offset);
return result.items.map((it) => {
const item = new vscode.CompletionItem(it.label, KIND[it.kind] || vscode.CompletionItemKind.Text);
item.detail = it.detail;
if (it.sort) item.sortText = it.sort + it.label;
if (it.documentation) item.documentation = new vscode.MarkdownString(it.documentation);
return item;
});
},
},
'.', ':', ' ',
);
}
function registerHover() {
return vscode.languages.registerHoverProvider({ language: 'dssl' }, {
provideHover(document, position) {
if (!analysis || !mod) return null;
const text = document.getText();
const offset = document.offsetAt(position);
const h = mod.hoverAt(analysis, document.uri.fsPath, text, offset);
if (!h) return null;
return new vscode.Hover(new vscode.MarkdownString(h.contents));
},
});
}
/* ---- the designer panel ---------------------------------------------- */
/** the .dssl editor to talk to: the last one the user was in, if it is still on screen */
function findDsslEditor() {
const visible = vscode.window.visibleTextEditors.filter((e) => e.document.languageId === 'dssl');
if (lastDsslEditor && visible.includes(lastDsslEditor)) return lastDsslEditor;
return visible[0] || null;
}
/** the editor, as designer-host.js wants it. New lines keep the indentation of the line you insert on. */
function editorBridge() {
return {
activeDsslEditor() {
const ed = findDsslEditor();
if (!ed) return null;
return {
async insert(text) {
const sel = ed.selection;
const indent = (ed.document.lineAt(sel.start.line).text.match(/^\s*/) || [''])[0];
const body = text.replace(/\n/g, '\n' + indent);
await ed.edit((b) => (sel.isEmpty ? b.insert(sel.active, body) : b.replace(sel, body)));
},
selectionText() { return ed.document.getText(ed.selection); },
};
},
copy: (text) => vscode.env.clipboard.writeText(text),
info: (m) => vscode.window.setStatusBarMessage('DSSL designer: ' + m, 4000),
post: (m) => designerPanel && designerPanel.webview.postMessage(m),
};
}
async function openDesigner(context, tab) {
if (!designerHost) designerHost = await import('./lib/designer-host.js');
if (designerPanel) {
designerPanel.reveal(undefined, true);
designerPanel.webview.postMessage({ type: 'tab', tab });
return;
}
const column = vscode.window.activeTextEditor ? vscode.ViewColumn.Beside : vscode.ViewColumn.Active;
designerPanel = vscode.window.createWebviewPanel('dsslDesigner', 'DSSL designer', { viewColumn: column, preserveFocus: true }, {
enableScripts: true,
retainContextWhenHidden: true,
localResourceRoots: [context.extensionUri],
});
const uri = (rel) => designerPanel.webview.asWebviewUri(vscode.Uri.joinPath(context.extensionUri, ...rel.split('/'))).toString();
designerPanel.webview.html = designerHost.designerHtml({ cspSource: designerPanel.webview.cspSource, uri, tab });
const bridge = editorBridge();
designerPanel.webview.onDidReceiveMessage((msg) => designerHost.handleMessage(msg, bridge).catch((e) => vscode.window.showErrorMessage('DSSL designer: ' + e.message)));
designerPanel.onDidDispose(() => { designerPanel = null; });
}
/* ---- CSS to DSSL -------------------------------------------------------- */
let importChannel = null;
async function convertCss(uri) {
const target = uri || (vscode.window.activeTextEditor && vscode.window.activeTextEditor.document.uri);
if (!target || !/\.css$/i.test(target.fsPath)) { vscode.window.showInformationMessage('DSSL: pick a .css file to convert (right-click it in the Explorer, or open it first).'); return; }
const { importCss } = await import('./lib/engine/import.js');
const text = Buffer.from(await vscode.workspace.fs.readFile(target)).toString('utf8');
const { dssl, report } = importCss(text);
const out = vscode.Uri.file(target.fsPath.replace(/\.css$/i, '.dssl'));
let exists = true;
try { await vscode.workspace.fs.stat(out); } catch { exists = false; }
if (exists) {
const pick = await vscode.window.showWarningMessage(`${out.fsPath.split(/[\\/]/).pop()} already exists.`, { modal: true }, 'Overwrite');
if (pick !== 'Overwrite') return;
}
await vscode.workspace.fs.writeFile(out, Buffer.from(dssl, 'utf8'));
if (!importChannel) importChannel = vscode.window.createOutputChannel('DSSL import');
importChannel.clear();
importChannel.appendLine(`Converted ${target.fsPath}`);
importChannel.appendLine(` ${report.rules} rule(s), ${report.declarations} declaration(s), ${report.converted} rewritten, ${report.pinned} moved to pin { }, ${report.motions} motion(s), ${report.raw} at-rule(s) kept in raw { }`);
for (const d of report.dropped) importChannel.appendLine(` dropped line ${d.line}: ${d.text}\n ${d.reason}`);
for (const n of report.notes) importChannel.appendLine(` note line ${n.line}: ${n.message}${n.hint ? '\n ' + n.hint : ''}`);
importChannel.appendLine(report.compile.ok ? ' The result compiles.' : ` The result still has ${report.compile.errors.length} error(s).`);
await vscode.window.showTextDocument(await vscode.workspace.openTextDocument(out), { preview: false });
const bits = [`${report.converted} values rewritten`];
if (report.dropped.length) bits.push(`${report.dropped.length} line(s) dropped`);
vscode.window.showInformationMessage(`DSSL: converted to ${out.fsPath.split(/[\\/]/).pop()} (${bits.join(', ')}).`, 'Show the report').then((c) => { if (c) importChannel.show(true); });
}
async function activate(context) {
mod = await import('./lib/analysis.js');
diags = vscode.languages.createDiagnosticCollection('dssl');
context.subscriptions.push(diags);
context.subscriptions.push(
vscode.workspace.onDidChangeTextDocument((e) => {
if (e.document.languageId === 'dssl') scheduleAnalysis();
}),
vscode.workspace.onDidOpenTextDocument((d) => { if (d.languageId === 'dssl') scheduleAnalysis(); }),
vscode.workspace.onDidChangeWorkspaceFolders(() => scheduleAnalysis()),
registerCompletion(),
registerHover(),
vscode.window.onDidChangeActiveTextEditor((ed) => { if (ed && ed.document.languageId === 'dssl') lastDsslEditor = ed; }),
vscode.commands.registerCommand('dssl.openDesigner', () => openDesigner(context, 'aura')),
vscode.commands.registerCommand('dssl.openAuraDesigner', () => openDesigner(context, 'aura')),
vscode.commands.registerCommand('dssl.openTextureDesigner', () => openDesigner(context, 'texture')),
vscode.commands.registerCommand('dssl.openSliderDesigner', () => openDesigner(context, 'slider')),
vscode.commands.registerCommand('dssl.convertCss', (uri) => convertCss(uri)),
);
if (vscode.window.activeTextEditor && vscode.window.activeTextEditor.document.languageId === 'dssl') lastDsslEditor = vscode.window.activeTextEditor;
// run once so diagnostics are present before the first keystroke
await runAnalysis();
}
function deactivate() {
clearTimeout(debounceTimer);
}
module.exports = { activate, deactivate };