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.
- package/.github/workflows/npm-publish.yml +30 -0
- package/.github/workflows/release.yml +84 -0
- package/CODE_OF_CONDUCT.md +10 -0
- package/FlowQueryLogoIcon.png +0 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/SECURITY.md +14 -0
- package/SUPPORT.md +13 -0
- package/docs/flowquery.min.js +1 -0
- package/docs/index.html +105 -0
- package/flowquery-vscode/.vscode-test.mjs +5 -0
- package/flowquery-vscode/.vscodeignore +13 -0
- package/flowquery-vscode/LICENSE +21 -0
- package/flowquery-vscode/README.md +11 -0
- package/flowquery-vscode/demo/FlowQueryVSCodeDemo.gif +0 -0
- package/flowquery-vscode/eslint.config.mjs +25 -0
- package/flowquery-vscode/extension.js +508 -0
- package/flowquery-vscode/flowQueryEngine/flowquery.min.js +1 -0
- package/flowquery-vscode/flowquery-worker.js +66 -0
- package/flowquery-vscode/images/FlowQueryLogoIcon.png +0 -0
- package/flowquery-vscode/jsconfig.json +13 -0
- package/flowquery-vscode/libs/page.css +53 -0
- package/flowquery-vscode/libs/table.css +13 -0
- package/flowquery-vscode/libs/tabs.css +66 -0
- package/flowquery-vscode/package-lock.json +2917 -0
- package/flowquery-vscode/package.json +51 -0
- package/flowquery-vscode/test/extension.test.js +196 -0
- package/flowquery-vscode/test/worker.test.js +25 -0
- package/flowquery-vscode/vsc-extension-quickstart.md +42 -0
- package/jest.config.js +11 -0
- package/package.json +28 -0
- package/queries/analyze_catfacts.cql +75 -0
- package/queries/azure_openai_completions.cql +13 -0
- package/queries/azure_openai_models.cql +9 -0
- package/queries/mock_pipeline.cql +84 -0
- package/queries/openai_completions.cql +15 -0
- package/queries/openai_models.cql +13 -0
- package/queries/test.cql +6 -0
- package/queries/tool_inference.cql +24 -0
- package/queries/wisdom.cql +6 -0
- package/queries/wisdom_letter_histogram.cql +8 -0
- package/src/compute/runner.ts +65 -0
- package/src/index.browser.ts +11 -0
- package/src/index.ts +12 -0
- package/src/io/command_line.ts +74 -0
- package/src/parsing/alias.ts +23 -0
- package/src/parsing/alias_option.ts +5 -0
- package/src/parsing/ast_node.ts +153 -0
- package/src/parsing/base_parser.ts +92 -0
- package/src/parsing/components/csv.ts +9 -0
- package/src/parsing/components/from.ts +12 -0
- package/src/parsing/components/headers.ts +12 -0
- package/src/parsing/components/json.ts +9 -0
- package/src/parsing/components/null.ts +9 -0
- package/src/parsing/components/post.ts +9 -0
- package/src/parsing/components/text.ts +9 -0
- package/src/parsing/context.ts +48 -0
- package/src/parsing/data_structures/associative_array.ts +43 -0
- package/src/parsing/data_structures/json_array.ts +31 -0
- package/src/parsing/data_structures/key_value_pair.ts +37 -0
- package/src/parsing/data_structures/lookup.ts +40 -0
- package/src/parsing/data_structures/range_lookup.ts +36 -0
- package/src/parsing/expressions/expression.ts +142 -0
- package/src/parsing/expressions/f_string.ts +26 -0
- package/src/parsing/expressions/identifier.ts +22 -0
- package/src/parsing/expressions/number.ts +40 -0
- package/src/parsing/expressions/operator.ts +179 -0
- package/src/parsing/expressions/reference.ts +42 -0
- package/src/parsing/expressions/string.ts +34 -0
- package/src/parsing/functions/aggregate_function.ts +58 -0
- package/src/parsing/functions/avg.ts +37 -0
- package/src/parsing/functions/collect.ts +44 -0
- package/src/parsing/functions/function.ts +60 -0
- package/src/parsing/functions/function_factory.ts +66 -0
- package/src/parsing/functions/join.ts +26 -0
- package/src/parsing/functions/predicate_function.ts +44 -0
- package/src/parsing/functions/predicate_function_factory.ts +15 -0
- package/src/parsing/functions/predicate_sum.ts +29 -0
- package/src/parsing/functions/rand.ts +13 -0
- package/src/parsing/functions/range.ts +18 -0
- package/src/parsing/functions/reducer_element.ts +10 -0
- package/src/parsing/functions/replace.ts +19 -0
- package/src/parsing/functions/round.ts +17 -0
- package/src/parsing/functions/size.ts +17 -0
- package/src/parsing/functions/split.ts +26 -0
- package/src/parsing/functions/stringify.ts +26 -0
- package/src/parsing/functions/sum.ts +31 -0
- package/src/parsing/functions/to_json.ts +17 -0
- package/src/parsing/functions/value_holder.ts +13 -0
- package/src/parsing/logic/case.ts +26 -0
- package/src/parsing/logic/else.ts +12 -0
- package/src/parsing/logic/end.ts +9 -0
- package/src/parsing/logic/then.ts +12 -0
- package/src/parsing/logic/when.ts +12 -0
- package/src/parsing/operations/aggregated_return.ts +18 -0
- package/src/parsing/operations/aggregated_with.ts +18 -0
- package/src/parsing/operations/group_by.ts +124 -0
- package/src/parsing/operations/limit.ts +22 -0
- package/src/parsing/operations/load.ts +92 -0
- package/src/parsing/operations/operation.ts +65 -0
- package/src/parsing/operations/projection.ts +18 -0
- package/src/parsing/operations/return.ts +43 -0
- package/src/parsing/operations/unwind.ts +32 -0
- package/src/parsing/operations/where.ts +38 -0
- package/src/parsing/operations/with.ts +20 -0
- package/src/parsing/parser.ts +762 -0
- package/src/parsing/token_to_node.ts +91 -0
- package/src/tokenization/keyword.ts +43 -0
- package/src/tokenization/operator.ts +25 -0
- package/src/tokenization/string_walker.ts +194 -0
- package/src/tokenization/symbol.ts +15 -0
- package/src/tokenization/token.ts +633 -0
- package/src/tokenization/token_mapper.ts +53 -0
- package/src/tokenization/token_type.ts +15 -0
- package/src/tokenization/tokenizer.ts +229 -0
- package/src/tokenization/trie.ts +117 -0
- package/src/utils/object_utils.ts +17 -0
- package/src/utils/string_utils.ts +114 -0
- package/tests/compute/runner.test.ts +498 -0
- package/tests/parsing/context.test.ts +27 -0
- package/tests/parsing/expression.test.ts +40 -0
- package/tests/parsing/parser.test.ts +434 -0
- package/tests/tokenization/token_mapper.test.ts +47 -0
- package/tests/tokenization/tokenizer.test.ts +67 -0
- package/tests/tokenization/trie.test.ts +20 -0
- package/tsconfig.json +15 -0
- package/typedoc.json +16 -0
- 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, '<')
|
|
278
|
+
.replace(/>/g, '>')
|
|
279
|
+
.replace(/"/g, '"')
|
|
280
|
+
.replace(/'/g, ''');
|
|
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, '&')
|
|
403
|
+
.replace(/</g, '<')
|
|
404
|
+
.replace(/>/g, '>');
|
|
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
|
+
};
|