flowquery 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/.github/workflows/npm-publish.yml +30 -0
  2. package/.github/workflows/release.yml +84 -0
  3. package/CODE_OF_CONDUCT.md +10 -0
  4. package/FlowQueryLogoIcon.png +0 -0
  5. package/LICENSE +21 -0
  6. package/README.md +113 -0
  7. package/SECURITY.md +14 -0
  8. package/SUPPORT.md +13 -0
  9. package/docs/flowquery.min.js +1 -0
  10. package/docs/index.html +105 -0
  11. package/flowquery-vscode/.vscode-test.mjs +5 -0
  12. package/flowquery-vscode/.vscodeignore +13 -0
  13. package/flowquery-vscode/LICENSE +21 -0
  14. package/flowquery-vscode/README.md +11 -0
  15. package/flowquery-vscode/demo/FlowQueryVSCodeDemo.gif +0 -0
  16. package/flowquery-vscode/eslint.config.mjs +25 -0
  17. package/flowquery-vscode/extension.js +508 -0
  18. package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -0
  19. package/flowquery-vscode/flowquery-worker.js +66 -0
  20. package/flowquery-vscode/images/FlowQueryLogoIcon.png +0 -0
  21. package/flowquery-vscode/jsconfig.json +13 -0
  22. package/flowquery-vscode/libs/page.css +53 -0
  23. package/flowquery-vscode/libs/table.css +13 -0
  24. package/flowquery-vscode/libs/tabs.css +66 -0
  25. package/flowquery-vscode/package-lock.json +2917 -0
  26. package/flowquery-vscode/package.json +51 -0
  27. package/flowquery-vscode/test/extension.test.js +196 -0
  28. package/flowquery-vscode/test/worker.test.js +25 -0
  29. package/flowquery-vscode/vsc-extension-quickstart.md +42 -0
  30. package/jest.config.js +11 -0
  31. package/package.json +28 -0
  32. package/queries/analyze_catfacts.cql +75 -0
  33. package/queries/azure_openai_completions.cql +13 -0
  34. package/queries/azure_openai_models.cql +9 -0
  35. package/queries/mock_pipeline.cql +84 -0
  36. package/queries/openai_completions.cql +15 -0
  37. package/queries/openai_models.cql +13 -0
  38. package/queries/test.cql +6 -0
  39. package/queries/tool_inference.cql +24 -0
  40. package/queries/wisdom.cql +6 -0
  41. package/queries/wisdom_letter_histogram.cql +8 -0
  42. package/src/compute/runner.ts +65 -0
  43. package/src/index.browser.ts +11 -0
  44. package/src/index.ts +12 -0
  45. package/src/io/command_line.ts +74 -0
  46. package/src/parsing/alias.ts +23 -0
  47. package/src/parsing/alias_option.ts +5 -0
  48. package/src/parsing/ast_node.ts +153 -0
  49. package/src/parsing/base_parser.ts +92 -0
  50. package/src/parsing/components/csv.ts +9 -0
  51. package/src/parsing/components/from.ts +12 -0
  52. package/src/parsing/components/headers.ts +12 -0
  53. package/src/parsing/components/json.ts +9 -0
  54. package/src/parsing/components/null.ts +9 -0
  55. package/src/parsing/components/post.ts +9 -0
  56. package/src/parsing/components/text.ts +9 -0
  57. package/src/parsing/context.ts +48 -0
  58. package/src/parsing/data_structures/associative_array.ts +43 -0
  59. package/src/parsing/data_structures/json_array.ts +31 -0
  60. package/src/parsing/data_structures/key_value_pair.ts +37 -0
  61. package/src/parsing/data_structures/lookup.ts +40 -0
  62. package/src/parsing/data_structures/range_lookup.ts +36 -0
  63. package/src/parsing/expressions/expression.ts +142 -0
  64. package/src/parsing/expressions/f_string.ts +26 -0
  65. package/src/parsing/expressions/identifier.ts +22 -0
  66. package/src/parsing/expressions/number.ts +40 -0
  67. package/src/parsing/expressions/operator.ts +179 -0
  68. package/src/parsing/expressions/reference.ts +42 -0
  69. package/src/parsing/expressions/string.ts +34 -0
  70. package/src/parsing/functions/aggregate_function.ts +58 -0
  71. package/src/parsing/functions/avg.ts +37 -0
  72. package/src/parsing/functions/collect.ts +44 -0
  73. package/src/parsing/functions/function.ts +60 -0
  74. package/src/parsing/functions/function_factory.ts +66 -0
  75. package/src/parsing/functions/join.ts +26 -0
  76. package/src/parsing/functions/predicate_function.ts +44 -0
  77. package/src/parsing/functions/predicate_function_factory.ts +15 -0
  78. package/src/parsing/functions/predicate_sum.ts +29 -0
  79. package/src/parsing/functions/rand.ts +13 -0
  80. package/src/parsing/functions/range.ts +18 -0
  81. package/src/parsing/functions/reducer_element.ts +10 -0
  82. package/src/parsing/functions/replace.ts +19 -0
  83. package/src/parsing/functions/round.ts +17 -0
  84. package/src/parsing/functions/size.ts +17 -0
  85. package/src/parsing/functions/split.ts +26 -0
  86. package/src/parsing/functions/stringify.ts +26 -0
  87. package/src/parsing/functions/sum.ts +31 -0
  88. package/src/parsing/functions/to_json.ts +17 -0
  89. package/src/parsing/functions/value_holder.ts +13 -0
  90. package/src/parsing/logic/case.ts +26 -0
  91. package/src/parsing/logic/else.ts +12 -0
  92. package/src/parsing/logic/end.ts +9 -0
  93. package/src/parsing/logic/then.ts +12 -0
  94. package/src/parsing/logic/when.ts +12 -0
  95. package/src/parsing/operations/aggregated_return.ts +18 -0
  96. package/src/parsing/operations/aggregated_with.ts +18 -0
  97. package/src/parsing/operations/group_by.ts +124 -0
  98. package/src/parsing/operations/limit.ts +22 -0
  99. package/src/parsing/operations/load.ts +92 -0
  100. package/src/parsing/operations/operation.ts +65 -0
  101. package/src/parsing/operations/projection.ts +18 -0
  102. package/src/parsing/operations/return.ts +43 -0
  103. package/src/parsing/operations/unwind.ts +32 -0
  104. package/src/parsing/operations/where.ts +38 -0
  105. package/src/parsing/operations/with.ts +20 -0
  106. package/src/parsing/parser.ts +762 -0
  107. package/src/parsing/token_to_node.ts +91 -0
  108. package/src/tokenization/keyword.ts +43 -0
  109. package/src/tokenization/operator.ts +25 -0
  110. package/src/tokenization/string_walker.ts +194 -0
  111. package/src/tokenization/symbol.ts +15 -0
  112. package/src/tokenization/token.ts +633 -0
  113. package/src/tokenization/token_mapper.ts +53 -0
  114. package/src/tokenization/token_type.ts +15 -0
  115. package/src/tokenization/tokenizer.ts +229 -0
  116. package/src/tokenization/trie.ts +117 -0
  117. package/src/utils/object_utils.ts +17 -0
  118. package/src/utils/string_utils.ts +114 -0
  119. package/tests/compute/runner.test.ts +498 -0
  120. package/tests/parsing/context.test.ts +27 -0
  121. package/tests/parsing/expression.test.ts +40 -0
  122. package/tests/parsing/parser.test.ts +434 -0
  123. package/tests/tokenization/token_mapper.test.ts +47 -0
  124. package/tests/tokenization/tokenizer.test.ts +67 -0
  125. package/tests/tokenization/trie.test.ts +20 -0
  126. package/tsconfig.json +15 -0
  127. package/typedoc.json +16 -0
  128. package/webpack.config.js +26 -0
@@ -0,0 +1,25 @@
1
+ import globals from "globals";
2
+
3
+ export default [{
4
+ files: ["**/*.js"],
5
+ languageOptions: {
6
+ globals: {
7
+ ...globals.commonjs,
8
+ ...globals.node,
9
+ ...globals.mocha,
10
+ },
11
+
12
+ ecmaVersion: 2022,
13
+ sourceType: "module",
14
+ },
15
+
16
+ rules: {
17
+ "no-const-assign": "warn",
18
+ "no-this-before-super": "warn",
19
+ "no-undef": "warn",
20
+ "no-unreachable": "warn",
21
+ "no-unused-vars": "warn",
22
+ "constructor-super": "warn",
23
+ "valid-typeof": "warn",
24
+ },
25
+ }];
@@ -0,0 +1,508 @@
1
+ const vscode = require('vscode');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ // Ensure UMD bundles that reference `self` work in the Node-based test / extension host.
5
+ if (typeof globalThis !== 'undefined' && typeof globalThis.self === 'undefined') {
6
+ globalThis.self = globalThis;
7
+ }
8
+ // FlowQuery execution is performed in a dedicated worker (`flowquery-worker.js`);
9
+
10
+ /**
11
+ * @param {vscode.ExtensionContext} context
12
+ */
13
+ function activate(context) {
14
+
15
+ const outputChannel = vscode.window.createOutputChannel('FlowQuery Results');
16
+ // Expose the channel for tests to wrap/inspect if needed.
17
+ module.exports._outputChannel = outputChannel;
18
+ // Expose a slot for the currently running worker/process and a test helper to cancel it
19
+ module.exports._lastWorker = null;
20
+ module.exports._cancelCurrentlyRunningQuery = function() {
21
+ try {
22
+ const w = module.exports._lastWorker;
23
+ if (!w) return false;
24
+ const anyW = /** @type {any} */ (w);
25
+ if (typeof anyW.terminate === 'function') {
26
+ anyW.terminate();
27
+ } else if (typeof anyW.kill === 'function') {
28
+ anyW.kill();
29
+ }
30
+ module.exports._lastWorker = null;
31
+ return true;
32
+ } catch {
33
+ return false;
34
+ }
35
+ };
36
+
37
+ // activation
38
+
39
+ async function runQueryOnDocument(documentText, documentPath) {
40
+ // If a .env file exists next to the documentPath, substitute any $KEY
41
+ // occurrences in the documentText with values from the .env file. Keys in
42
+ // the .env file are plain (do not include the leading $); any $KEY tokens
43
+ // in the query will be replaced with the corresponding value (raw, no
44
+ // extra quoting is added).
45
+ function applyEnvSubstitutions(text, docPath) {
46
+ if (!docPath) return text;
47
+ try {
48
+ const dir = path.dirname(docPath);
49
+ const envPath = path.join(dir, '.env');
50
+ if (!fs.existsSync(envPath)) return text;
51
+ const raw = fs.readFileSync(envPath, 'utf8');
52
+ const map = Object.create(null);
53
+ raw.split(/\r?\n/).forEach(line => {
54
+ const trimmed = line.trim();
55
+ if (!trimmed || trimmed.startsWith('#')) return;
56
+ const idx = trimmed.indexOf('=');
57
+ if (idx === -1) return;
58
+ const key = trimmed.substring(0, idx).trim();
59
+ let val = trimmed.substring(idx + 1).trim();
60
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
61
+ val = val.substring(1, val.length - 1);
62
+ }
63
+ map[key] = val;
64
+ });
65
+
66
+ return text.replace(/\$([A-Za-z0-9_]+)/g, (match, key) => {
67
+ if (Object.prototype.hasOwnProperty.call(map, key)) return map[key];
68
+ return match; // leave unchanged if not found
69
+ });
70
+ } catch {
71
+ try { module.exports._outputChannel && module.exports._outputChannel.appendLine && module.exports._outputChannel.appendLine('Warning: .env could not be read or parsed.'); } catch {}
72
+ return text;
73
+ }
74
+ }
75
+
76
+ // perform substitutions (no-op if documentPath is not provided or file doesn't exist)
77
+ documentText = applyEnvSubstitutions(documentText, documentPath);
78
+ try {
79
+ // runQueryOnDocument started
80
+ // Clear any previous output and show a running message while the query executes.
81
+ outputChannel.clear();
82
+ outputChannel.appendLine('Running FlowQuery statement. Please wait...');
83
+ outputChannel.show(true);
84
+
85
+ // running in worker instead of instantiating FlowQuery here
86
+ let isCancelled = false;
87
+ let workerResults = null;
88
+
89
+ // Use VS Code's withProgress API for loading indicator and cancellation support
90
+ await vscode.window.withProgress({
91
+ location: vscode.ProgressLocation.Notification,
92
+ title: 'Running FlowQuery statement...',
93
+ cancellable: true
94
+ }, async (progress, token) => {
95
+ progress.report({ increment: 0 });
96
+
97
+ token.onCancellationRequested(() => {
98
+ isCancelled = true;
99
+ outputChannel.appendLine('Query cancelled by user.');
100
+ vscode.window.showInformationMessage('FlowQuery query cancelled.');
101
+ });
102
+
103
+ // Execute the FlowQuery inside a worker so it can be terminated on cancellation.
104
+ workerResults = await new Promise(async (resolve, reject) => {
105
+ // Try to use worker_threads first
106
+ try {
107
+ const { Worker } = require('worker_threads');
108
+ const worker = new Worker(path.join(__dirname, 'flowquery-worker.js'));
109
+ let settled = false;
110
+
111
+ worker.on('message', (msg) => {
112
+ if (settled) return;
113
+ const m = /** @type {any} */ (msg);
114
+ if (m && m.type === 'results') {
115
+ settled = true;
116
+ resolve(m.results);
117
+ } else if (m && m.type === 'error') {
118
+ settled = true;
119
+ reject(new Error(m.message || 'Unknown error from worker'));
120
+ }
121
+ });
122
+
123
+ worker.on('error', (err) => {
124
+ if (!settled) {
125
+ settled = true;
126
+ reject(err);
127
+ }
128
+ });
129
+
130
+ // ensure we clear the last worker ref when the worker exits
131
+ worker.on('exit', (code) => {
132
+ if (module.exports._lastWorker === worker) module.exports._lastWorker = null;
133
+ if (!settled) {
134
+ settled = true;
135
+ if (code === 0) resolve(null);
136
+ else reject(new Error(`Worker exited with code ${code}`));
137
+ }
138
+ });
139
+
140
+ worker.postMessage(documentText);
141
+ // documentText already had YAML substitutions applied earlier
142
+ // expose worker to tests so they can cancel it programmatically
143
+ module.exports._lastWorker = worker;
144
+
145
+ token.onCancellationRequested(() => {
146
+ isCancelled = true;
147
+ try { worker.terminate(); } catch {}
148
+ if (!settled) {
149
+ settled = true;
150
+ resolve(null);
151
+ }
152
+ });
153
+
154
+ } catch {
155
+ // Fallback to child process fork if worker_threads is unavailable
156
+ try {
157
+ const cp = require('child_process');
158
+ const proc = cp.fork(path.join(__dirname, 'flowquery-worker.js'));
159
+ let settled = false;
160
+
161
+ proc.on('message', (msg) => {
162
+ if (settled) return;
163
+ const m = /** @type {any} */ (msg);
164
+ if (m && m.type === 'results') {
165
+ settled = true;
166
+ resolve(m.results);
167
+ } else if (m && m.type === 'error') {
168
+ settled = true;
169
+ reject(new Error(m.message || 'Unknown error from worker process'));
170
+ }
171
+ });
172
+
173
+ proc.on('error', (err) => {
174
+ if (!settled) {
175
+ settled = true;
176
+ reject(err);
177
+ }
178
+ });
179
+
180
+ // ensure we clear the last worker ref when the process exits
181
+ proc.on('exit', (code) => {
182
+ if (module.exports._lastWorker === proc) module.exports._lastWorker = null;
183
+ if (!settled) {
184
+ settled = true;
185
+ if (code === 0) resolve(null);
186
+ else reject(new Error(`Worker process exited with code ${code}`));
187
+ }
188
+ });
189
+
190
+ proc.send(documentText);
191
+ // documentText already had YAML substitutions applied earlier
192
+ // expose forked process for tests
193
+ module.exports._lastWorker = proc;
194
+
195
+ token.onCancellationRequested(() => {
196
+ isCancelled = true;
197
+ try { proc.kill(); } catch {}
198
+ if (!settled) {
199
+ settled = true;
200
+ resolve(null);
201
+ }
202
+ });
203
+
204
+ } catch (err2) {
205
+ reject(err2);
206
+ }
207
+ }
208
+ });
209
+ });
210
+
211
+ if (isCancelled) {
212
+ // If cancelled, don't show results.
213
+ outputChannel.appendLine('Query was cancelled before completion.');
214
+ // query cancelled
215
+ return;
216
+ }
217
+
218
+ if (!workerResults) {
219
+ outputChannel.appendLine('No results returned from worker.');
220
+ vscode.window.showErrorMessage('No results returned from FlowQuery execution.');
221
+ // no worker results
222
+ return;
223
+ }
224
+
225
+ // have worker results
226
+ // At this point the query has completed successfully; show results in a webview using drawTable
227
+ const panel = vscode.window.createWebviewPanel(
228
+ 'flowqueryResults',
229
+ 'FlowQuery Results',
230
+ vscode.ViewColumn.Beside,
231
+ { enableScripts: true }
232
+ );
233
+
234
+ panel.webview.html = getWebviewContent(workerResults, panel.webview, context.extensionUri);
235
+
236
+ // Handle messages posted from the webview (e.g. copy confirmation)
237
+ panel.webview.onDidReceiveMessage(message => {
238
+ try {
239
+ if (message && message.command === 'copied') {
240
+ vscode.window.showInformationMessage('Content copied to clipboard');
241
+ }
242
+ } catch {
243
+ // swallow errors from message handling
244
+ }
245
+ });
246
+
247
+ const resultString = JSON.stringify(workerResults, null, 2);
248
+ outputChannel.clear();
249
+ outputChannel.appendLine(resultString);
250
+ outputChannel.show(true);
251
+ } catch (e) {
252
+ console.error(e);
253
+ // Clear running message and show error details in the output channel as well as a popup.
254
+ outputChannel.clear();
255
+ outputChannel.appendLine(`Query failed: ${e.message}`);
256
+ outputChannel.show(true);
257
+ vscode.window.showErrorMessage(`Query failed: ${e.message}`);
258
+ }
259
+ }
260
+
261
+ let disposable = vscode.commands.registerCommand('extension.runFlowQueryStatement', () => {
262
+ const editor = vscode.window.activeTextEditor;
263
+ if (editor) {
264
+ const text = editor.document.getText();
265
+ // pass the active document path so .env.yaml lookup can occur in the same folder
266
+ runQueryOnDocument(text, editor.document.uri && editor.document.uri.fsPath);
267
+ }
268
+ });
269
+
270
+ context.subscriptions.push(disposable);
271
+ }
272
+
273
+ function escapeHtml(unsafe) {
274
+ if (unsafe === null || unsafe === undefined) return '';
275
+ return String(unsafe)
276
+ .replace(/&/g, '&')
277
+ .replace(/</g, '&lt;')
278
+ .replace(/>/g, '&gt;')
279
+ .replace(/"/g, '&quot;')
280
+ .replace(/'/g, '&#39;');
281
+ }
282
+
283
+ function drawTable(results) {
284
+ // Support both shapes: worker sends an array of result objects directly, or
285
+ // an object with an `output` property (older format).
286
+ const output = Array.isArray(results) ? results : (results && results.output);
287
+ const hasOutput = output && output.length;
288
+
289
+ if (hasOutput) {
290
+ const columns = Object.keys(output[0]);
291
+
292
+ let table = '<table>';
293
+ table += '<tr>';
294
+ columns.forEach(column => {
295
+ table += `<th>${escapeHtml(column)}</th>`;
296
+ });
297
+ table += '</tr>';
298
+ for (let i = 0; i < output.length; i++) {
299
+ table += '<tr>';
300
+ for (let j = 0; j < columns.length; j++) {
301
+ let value = output[i][columns[j]];
302
+ if (typeof value === 'object') {
303
+ try {
304
+ value = JSON.stringify(value);
305
+ } catch {
306
+ value = String(value);
307
+ }
308
+ } else if (typeof value === 'boolean') {
309
+ value = value ? 'true' : 'false';
310
+ } else if (value === null) {
311
+ value = 'null';
312
+ } else if (value === undefined) {
313
+ value = 'undefined';
314
+ } else {
315
+ value = value.toString();
316
+ }
317
+ table += `<td>${escapeHtml(value)}</td>`;
318
+ }
319
+ table += '</tr>';
320
+ }
321
+ table += '</table>';
322
+
323
+ return table;
324
+ }
325
+ return '';
326
+ }
327
+
328
+ function getPath(webview, _path, file) {
329
+ return webview.asWebviewUri(vscode.Uri.joinPath(_path, file));
330
+ }
331
+
332
+ function getWebviewContent(results, webview, extensionUri) {
333
+ const libsPath = vscode.Uri.joinPath(extensionUri, 'libs');
334
+ const safeResults = JSON.stringify(results).replace(/</g, '\\u003c');
335
+ return `
336
+ <!DOCTYPE html>
337
+ <html lang="en">
338
+ <head>
339
+ <meta charset="UTF-8">
340
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
341
+ <link rel="stylesheet" type="text/css" href="${getPath(webview, libsPath, 'page.css')}">
342
+ <link rel="stylesheet" type="text/css" href="${getPath(webview, libsPath, 'tabs.css')}">
343
+ <link rel="stylesheet" type="text/css" href="${getPath(webview, libsPath, 'table.css')}">
344
+ </head>
345
+ <body>
346
+ <div class="tab">
347
+ <button class="tab-links active" onclick="openTab(event, 'table')">Table</button>
348
+ <button class="tab-links" onclick="openTab(event, 'json')">JSON</button>
349
+ </div>
350
+
351
+ <div id="table" class="tab-content active">
352
+ <span id="table-data">
353
+ ${drawTable(results)}
354
+ </span>
355
+ <button class="copy-button" onclick="copyToClipboard('table-data')">Copy</button>
356
+ </div>
357
+
358
+ <div id="json" class="tab-content">
359
+ <div class="json-controls">
360
+ <button id="wrap-toggle" onclick="toggleWrap()">No wrap</button>
361
+ </div>
362
+ <pre id="json-content"></pre>
363
+ <button class="copy-button" onclick="copyToClipboard('json-content')">Copy</button>
364
+ </div>
365
+
366
+ <script>
367
+ const vscodeApi = acquireVsCodeApi();
368
+ function openTab(evt, tabName) {
369
+ const tabContents = document.getElementsByClassName("tab-content");
370
+ const tabLinks = document.getElementsByClassName("tab-links");
371
+
372
+ // Hide all tab contents
373
+ for (let i = 0; i < tabContents.length; i++) {
374
+ tabContents[i].classList.remove("active");
375
+ }
376
+
377
+ // Remove active class from all tab buttons
378
+ for (let i = 0; i < tabLinks.length; i++) {
379
+ tabLinks[i].classList.remove("active");
380
+ }
381
+
382
+ // Show the selected tab and set active state on button
383
+ document.getElementById(tabName).classList.add("active");
384
+ evt.currentTarget.classList.add("active");
385
+ }
386
+
387
+ function copyToClipboard(elementId) {
388
+ const text = document.getElementById(elementId).innerText;
389
+ navigator.clipboard.writeText(text).then(() => {
390
+ // Notify the extension host so it can show a VS Code notification
391
+ vscodeApi.postMessage({ command: 'copied' });
392
+ }).catch(err => {
393
+ console.error('Could not copy text: ', err);
394
+ });
395
+ }
396
+
397
+ // ----- JSON UI helpers -----
398
+ const resultsData = ${safeResults};
399
+
400
+ function escapeHtmlForWeb(str) {
401
+ return String(str)
402
+ .replace(/&/g, '&amp;')
403
+ .replace(/</g, '&lt;')
404
+ .replace(/>/g, '&gt;');
405
+ }
406
+
407
+ function syntaxHighlight(json) {
408
+ if (!json) return '';
409
+ const escaped = escapeHtmlForWeb(json);
410
+ return escaped.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?)|\b(true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g, function (match) {
411
+ let cls = 'number';
412
+ if (/^"/.test(match)) {
413
+ if (/:\s*$/.test(match)) {
414
+ cls = 'key';
415
+ } else {
416
+ cls = 'string';
417
+ }
418
+ } else if (/true|false/.test(match)) {
419
+ cls = 'boolean';
420
+ } else if (/null/.test(match)) {
421
+ cls = 'null';
422
+ }
423
+ return '<span class="' + cls + '">' + match + '</span>';
424
+ });
425
+ }
426
+
427
+ function renderJson() {
428
+ const pretty = JSON.stringify(resultsData, null, 2);
429
+ const pre = document.getElementById('json-content');
430
+ pre.textContent = pretty;
431
+ pre.innerHTML = syntaxHighlight(pre.textContent);
432
+ }
433
+
434
+ function toggleWrap() {
435
+ const pre = document.getElementById('json-content');
436
+ const btn = document.getElementById('wrap-toggle');
437
+ if (pre.classList.contains('nowrap')) {
438
+ pre.classList.remove('nowrap');
439
+ btn.textContent = 'No wrap';
440
+ } else {
441
+ pre.classList.add('nowrap');
442
+ btn.textContent = 'Wrap';
443
+ }
444
+ // Persist nowrap state across webview sessions
445
+ const newState = Object.assign({}, vscodeApi.getState() || {}, { nowrap: pre.classList.contains('nowrap') });
446
+ vscodeApi.setState(newState);
447
+ }
448
+
449
+ // Initialize
450
+ (function() {
451
+ renderJson();
452
+ // restore persisted state if present
453
+ const state = vscodeApi.getState() || {};
454
+ const pre = document.getElementById('json-content');
455
+ const btn = document.getElementById('wrap-toggle');
456
+ if (state.nowrap) {
457
+ pre.classList.add('nowrap');
458
+ btn.textContent = 'Wrap';
459
+ } else {
460
+ pre.classList.remove('nowrap');
461
+ btn.textContent = 'No wrap';
462
+ }
463
+ })();
464
+
465
+ </script>
466
+ </body>
467
+ </html>
468
+ `;
469
+ }
470
+
471
+ function deactivate() {}
472
+
473
+ // Attach activate/deactivate to the existing exports object so test harness can access
474
+ // properties placed on module.exports (such as `_outputChannel`) earlier.
475
+ module.exports.activate = activate;
476
+ module.exports.deactivate = deactivate;
477
+
478
+ // Expose an applyEnvSubstitutions helper for unit tests
479
+ module.exports._applyEnvSubstitutions = function(text, docPath) {
480
+ if (!docPath) return text;
481
+ try {
482
+ const dir = path.dirname(docPath);
483
+ const envPath = path.join(dir, '.env');
484
+ if (!fs.existsSync(envPath)) return text;
485
+ const raw = fs.readFileSync(envPath, 'utf8');
486
+ const map = Object.create(null);
487
+ raw.split(/\r?\n/).forEach(line => {
488
+ const trimmed = line.trim();
489
+ if (!trimmed || trimmed.startsWith('#')) return;
490
+ const idx = trimmed.indexOf('=');
491
+ if (idx === -1) return;
492
+ const key = trimmed.substring(0, idx).trim();
493
+ let val = trimmed.substring(idx + 1).trim();
494
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
495
+ val = val.substring(1, val.length - 1);
496
+ }
497
+ map[key] = val;
498
+ });
499
+
500
+ return text.replace(/\$([A-Za-z0-9_]+)/g, (match, key) => {
501
+ if (Object.prototype.hasOwnProperty.call(map, key)) return map[key];
502
+ return match;
503
+ });
504
+ } catch {
505
+ try { module.exports._outputChannel && module.exports._outputChannel.appendLine && module.exports._outputChannel.appendLine('Warning: .env could not be read or parsed.'); } catch {}
506
+ return text;
507
+ }
508
+ };