File size: 9,834 Bytes
e92b981
2d39154
 
 
 
 
 
 
 
 
 
e92b981
 
 
2d39154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ee84700
2d39154
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e92b981
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2d39154
 
 
 
 
 
 
 
 
 
 
 
 
e92b981
 
 
 
 
 
2d39154
e92b981
2d39154
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
/* 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 };