norn-cli 1.3.16 → 1.3.18

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.
@@ -0,0 +1,1025 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.activate = activate;
37
+ exports.saveSchemaValidationResults = saveSchemaValidationResults;
38
+ exports.deactivate = deactivate;
39
+ const vscode = __importStar(require("vscode"));
40
+ const path = __importStar(require("path"));
41
+ const fs = __importStar(require("fs/promises"));
42
+ const fsSync = __importStar(require("fs"));
43
+ const parser_1 = require("./parser");
44
+ const httpClient_1 = require("./httpClient");
45
+ const codeLensProvider_1 = require("./codeLensProvider");
46
+ const completionProvider_1 = require("./completionProvider");
47
+ const responsePanel_1 = require("./responsePanel");
48
+ const diagnosticProvider_1 = require("./diagnosticProvider");
49
+ const sequenceRunner_1 = require("./sequenceRunner");
50
+ const environmentProvider_1 = require("./environmentProvider");
51
+ const swaggerParser_1 = require("./swaggerParser");
52
+ const nornapiParser_1 = require("./nornapiParser");
53
+ const testProvider_1 = require("./testProvider");
54
+ const coverageCalculator_1 = require("./coverageCalculator");
55
+ const coveragePanel_1 = require("./coveragePanel");
56
+ const compareContentProvider_1 = require("./compareContentProvider");
57
+ const contractDecorationProvider_1 = require("./contractDecorationProvider");
58
+ const validationCache_1 = require("./validationCache");
59
+ const schemaGenerator_1 = require("./schemaGenerator");
60
+ // Module-level reference to contract decoration provider for refreshing after sequence runs
61
+ let contractDecorationProviderInstance;
62
+ function activate(context) {
63
+ console.log('Norn extension activated');
64
+ // Register content provider for response comparison diff
65
+ (0, compareContentProvider_1.registerCompareContentProvider)(context);
66
+ // Register Test Explorer integration
67
+ const testController = new testProvider_1.NornTestController();
68
+ context.subscriptions.push(testController);
69
+ const sendRequestCommand = vscode.commands.registerCommand('norn.sendRequest', (lineFromCodeLens) => processEditorsInput(context.extensionUri, lineFromCodeLens));
70
+ const runSequenceCommand = vscode.commands.registerCommand('norn.runSequence', (lineFromCodeLens) => processSequence(context.extensionUri, lineFromCodeLens));
71
+ const clearCookiesCommand = vscode.commands.registerCommand('norn.clearCookies', async () => {
72
+ (0, httpClient_1.clearCookies)();
73
+ vscode.window.showInformationMessage('Norn: All cookies cleared');
74
+ });
75
+ const showCookiesCommand = vscode.commands.registerCommand('norn.showCookies', async () => {
76
+ const cookies = await (0, httpClient_1.getAllCookies)();
77
+ if (cookies.length === 0) {
78
+ vscode.window.showInformationMessage('Norn: No cookies stored');
79
+ }
80
+ else {
81
+ const items = cookies.map(c => `${c.domain}: ${c.name}=${c.value.substring(0, 30)}${c.value.length > 30 ? '...' : ''}`);
82
+ vscode.window.showQuickPick(items, { title: 'Stored Cookies', canPickMany: false });
83
+ }
84
+ });
85
+ // Register environment selector command
86
+ const selectEnvironmentCommand = vscode.commands.registerCommand('norn.selectEnvironment', async () => {
87
+ await (0, environmentProvider_1.showEnvironmentPicker)();
88
+ vscode.commands.executeCommand('norn.refreshDiagnostics');
89
+ });
90
+ // Register environment selector with refresh (for CodeLens)
91
+ const selectEnvironmentAndRefreshCommand = vscode.commands.registerCommand('norn.selectEnvironmentAndRefresh', async () => {
92
+ await (0, environmentProvider_1.showEnvironmentPicker)();
93
+ vscode.commands.executeCommand('norn.refreshCodeLenses');
94
+ vscode.commands.executeCommand('norn.refreshDiagnostics');
95
+ });
96
+ // Register command to create .nornenv file
97
+ const createEnvFileCommand = vscode.commands.registerCommand('norn.createEnvFile', async () => {
98
+ const workspaceFolders = vscode.workspace.workspaceFolders;
99
+ if (!workspaceFolders) {
100
+ vscode.window.showErrorMessage('No workspace folder open');
101
+ return;
102
+ }
103
+ // If multiple folders, ask which one
104
+ let targetFolder = workspaceFolders[0];
105
+ if (workspaceFolders.length > 1) {
106
+ const picked = await vscode.window.showWorkspaceFolderPick({
107
+ placeHolder: 'Select workspace folder for .nornenv file'
108
+ });
109
+ if (!picked) {
110
+ return;
111
+ }
112
+ targetFolder = picked;
113
+ }
114
+ const envFilePath = vscode.Uri.joinPath(targetFolder.uri, '.nornenv');
115
+ const template = `# Norn Environment Configuration
116
+ # Variables defined here are available in your .norn files as {{variableName}}
117
+
118
+ # Common variables (available in all environments)
119
+ var baseUrl = https://api.example.com
120
+
121
+ # Development environment
122
+ [env:dev]
123
+ var apiKey = dev-api-key-here
124
+ var debug = true
125
+
126
+ # Production environment
127
+ [env:prod]
128
+ var apiKey = prod-api-key-here
129
+ var debug = false
130
+ `;
131
+ try {
132
+ await vscode.workspace.fs.writeFile(envFilePath, Buffer.from(template, 'utf8'));
133
+ const doc = await vscode.workspace.openTextDocument(envFilePath);
134
+ await vscode.window.showTextDocument(doc);
135
+ vscode.commands.executeCommand('norn.refreshCodeLenses');
136
+ vscode.window.showInformationMessage('Created .nornenv file. Select an environment from the status bar.');
137
+ }
138
+ catch (error) {
139
+ vscode.window.showErrorMessage(`Failed to create .nornenv: ${error}`);
140
+ }
141
+ });
142
+ // Register command to import endpoints from Swagger/OpenAPI spec
143
+ const importSwaggerCommand = vscode.commands.registerCommand('norn.importSwagger', async (swaggerUrl, lineNumber) => {
144
+ const editor = vscode.window.activeTextEditor;
145
+ if (!editor) {
146
+ vscode.window.showErrorMessage('No active editor');
147
+ return;
148
+ }
149
+ try {
150
+ // Show progress while fetching
151
+ await vscode.window.withProgress({
152
+ location: vscode.ProgressLocation.Notification,
153
+ title: 'Parsing OpenAPI spec...',
154
+ cancellable: false
155
+ }, async (progress) => {
156
+ progress.report({ message: 'Fetching specification...' });
157
+ // Parse the swagger spec
158
+ const spec = await (0, swaggerParser_1.parseSwaggerSpec)(swaggerUrl);
159
+ progress.report({ message: 'Spec loaded!' });
160
+ // Build QuickPick items for sections
161
+ const sectionItems = [
162
+ { label: 'All', description: `Import all ${spec.sections.reduce((acc, s) => acc + s.endpoints.length, 0)} endpoints`, picked: true }
163
+ ];
164
+ for (const section of spec.sections) {
165
+ sectionItems.push({
166
+ label: section.name,
167
+ description: section.description || `${section.endpoints.length} endpoints`
168
+ });
169
+ }
170
+ // Show section picker
171
+ const selectedSections = await vscode.window.showQuickPick(sectionItems, {
172
+ canPickMany: true,
173
+ placeHolder: `Select sections to import from ${spec.title}`,
174
+ title: 'Import OpenAPI Endpoints'
175
+ });
176
+ if (!selectedSections || selectedSections.length === 0) {
177
+ return;
178
+ }
179
+ const sectionNames = selectedSections.map(s => s.label);
180
+ // Ask for custom baseUrl
181
+ const baseUrlInput = await vscode.window.showInputBox({
182
+ prompt: 'Enter custom base URL (or leave empty to use spec default)',
183
+ placeHolder: spec.baseUrl || 'https://api.example.com',
184
+ value: spec.baseUrl
185
+ });
186
+ // User cancelled
187
+ if (baseUrlInput === undefined) {
188
+ return;
189
+ }
190
+ // Generate the content
191
+ const content = (0, swaggerParser_1.generateNornapiContent)(spec, sectionNames, baseUrlInput || undefined);
192
+ // Find position to insert - after the swagger line
193
+ const document = editor.document;
194
+ const swaggerLine = document.lineAt(lineNumber);
195
+ const insertPosition = new vscode.Position(lineNumber + 1, 0);
196
+ // Insert the generated content
197
+ await editor.edit(editBuilder => {
198
+ editBuilder.insert(insertPosition, '\n' + content + '\n');
199
+ });
200
+ vscode.window.showInformationMessage(`Imported ${sectionNames.includes('All') ? 'all sections' : sectionNames.join(', ')} from ${spec.title}`);
201
+ });
202
+ }
203
+ catch (error) {
204
+ const errorMessage = error instanceof Error ? error.message : String(error);
205
+ vscode.window.showErrorMessage(`Failed to parse OpenAPI spec: ${errorMessage}`);
206
+ }
207
+ });
208
+ // Register command to generate JSON Schemas from Swagger/OpenAPI spec
209
+ const generateSchemasFromSwaggerCommand = vscode.commands.registerCommand('norn.generateSchemasFromSwagger', async (swaggerUrl, sourceFile) => {
210
+ try {
211
+ const schemas = await vscode.window.withProgress({
212
+ location: vscode.ProgressLocation.Notification,
213
+ title: 'Extracting schemas from OpenAPI spec...',
214
+ cancellable: false
215
+ }, async (progress) => {
216
+ progress.report({ message: 'Fetching specification...' });
217
+ return await (0, swaggerParser_1.extractResponseSchemas)(swaggerUrl);
218
+ });
219
+ if (schemas.length === 0) {
220
+ vscode.window.showWarningMessage('No response schemas found in the OpenAPI spec');
221
+ return;
222
+ }
223
+ // Ask user for output folder
224
+ const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
225
+ const defaultFolder = workspaceRoot ? path.join(workspaceRoot, 'contracts') : path.dirname(sourceFile);
226
+ const folderItems = [
227
+ { label: 'contracts/', description: `${defaultFolder}`, picked: true },
228
+ { label: 'schemas/', description: path.join(path.dirname(sourceFile), 'schemas') },
229
+ { label: 'Choose folder...', description: 'Select a custom folder' }
230
+ ];
231
+ const selectedFolder = await vscode.window.showQuickPick(folderItems, {
232
+ placeHolder: 'Select output folder for schema files',
233
+ title: `Generate ${schemas.length} Schemas`
234
+ });
235
+ if (!selectedFolder) {
236
+ return;
237
+ }
238
+ let outputFolder;
239
+ if (selectedFolder.label === 'Choose folder...') {
240
+ const folderUri = await vscode.window.showOpenDialog({
241
+ canSelectFiles: false,
242
+ canSelectFolders: true,
243
+ canSelectMany: false,
244
+ title: 'Select output folder for schemas'
245
+ });
246
+ if (!folderUri || folderUri.length === 0) {
247
+ return;
248
+ }
249
+ outputFolder = folderUri[0].fsPath;
250
+ }
251
+ else if (selectedFolder.label === 'contracts/') {
252
+ outputFolder = defaultFolder;
253
+ }
254
+ else {
255
+ outputFolder = path.join(path.dirname(sourceFile), 'schemas');
256
+ }
257
+ // Create folder if it doesn't exist
258
+ if (!fsSync.existsSync(outputFolder)) {
259
+ fsSync.mkdirSync(outputFolder, { recursive: true });
260
+ }
261
+ // Filter schemas to generate (allow user to select)
262
+ const schemaItems = schemas.map((s) => ({
263
+ label: (0, swaggerParser_1.generateSchemaFilename)(s.operationId, s.statusCode),
264
+ description: `${s.method} ${s.path} → ${s.statusCode}`,
265
+ picked: s.statusCode === '200' // Default select 200 responses
266
+ }));
267
+ const selectedSchemas = await vscode.window.showQuickPick(schemaItems, {
268
+ canPickMany: true,
269
+ placeHolder: 'Select schemas to generate',
270
+ title: `${schemas.length} response schemas available`
271
+ });
272
+ if (!selectedSchemas || selectedSchemas.length === 0) {
273
+ return;
274
+ }
275
+ const selectedFilenames = new Set(selectedSchemas.map(s => s.label));
276
+ // Write schema files
277
+ let createdCount = 0;
278
+ for (const schema of schemas) {
279
+ const filename = (0, swaggerParser_1.generateSchemaFilename)(schema.operationId, schema.statusCode);
280
+ if (!selectedFilenames.has(filename)) {
281
+ continue;
282
+ }
283
+ const filePath = path.join(outputFolder, filename);
284
+ const content = JSON.stringify(schema.schema, null, 2);
285
+ fsSync.writeFileSync(filePath, content, 'utf-8');
286
+ createdCount++;
287
+ }
288
+ vscode.window.showInformationMessage(`Generated ${createdCount} schema files in ${path.basename(outputFolder)}/`);
289
+ }
290
+ catch (error) {
291
+ const errorMessage = error instanceof Error ? error.message : String(error);
292
+ vscode.window.showErrorMessage(`Failed to generate schemas: ${errorMessage}`);
293
+ }
294
+ });
295
+ // === Coverage Feature Setup ===
296
+ // Create coverage status bar item
297
+ const coverageStatusBarItem = (0, environmentProvider_1.createCoverageStatusBarItem)();
298
+ // Register command to show coverage panel
299
+ const showCoverageCommand = vscode.commands.registerCommand('norn.showCoverage', async () => {
300
+ try {
301
+ const coverage = await (0, coverageCalculator_1.getCoverage)();
302
+ coveragePanel_1.CoveragePanel.show(coverage);
303
+ }
304
+ catch (error) {
305
+ const errorMessage = error instanceof Error ? error.message : String(error);
306
+ vscode.window.showErrorMessage(`Failed to calculate coverage: ${errorMessage}`);
307
+ }
308
+ });
309
+ // Register command to refresh coverage
310
+ const refreshCoverageCommand = vscode.commands.registerCommand('norn.refreshCoverage', async () => {
311
+ try {
312
+ (0, swaggerParser_1.invalidateSwaggerCache)();
313
+ const coverage = await (0, coverageCalculator_1.refreshCoverage)();
314
+ coveragePanel_1.CoveragePanel.updateContent(coverage);
315
+ vscode.window.showInformationMessage(`Coverage refreshed: ${coverage.percentage}%`);
316
+ }
317
+ catch (error) {
318
+ const errorMessage = error instanceof Error ? error.message : String(error);
319
+ vscode.window.showErrorMessage(`Failed to refresh coverage: ${errorMessage}`);
320
+ }
321
+ });
322
+ // Listen for coverage updates to update status bar and CodeLens
323
+ const coverageUpdateListener = (0, coverageCalculator_1.onCoverageUpdate)((coverage) => {
324
+ (0, environmentProvider_1.updateCoverageStatusBar)(coverage.hasSwagger, coverage.percentage, coverage.total, coverage.covered);
325
+ (0, codeLensProvider_1.updateCodeLensCoverage)(coverage.percentage, coverage.total, coverage.covered);
326
+ vscode.commands.executeCommand('norn.refreshCodeLenses');
327
+ });
328
+ // File watcher for .nornapi files - only recalculate when files are created/deleted
329
+ // (might add new swagger URLs that need fetching)
330
+ const nornapiFileWatcher = vscode.workspace.createFileSystemWatcher('**/*.nornapi');
331
+ const scheduleNornapiRefresh = debounce(async () => {
332
+ (0, coverageCalculator_1.clearCoverageCache)();
333
+ // Recalculate - this will fetch new swagger URLs if detected
334
+ try {
335
+ await (0, coverageCalculator_1.getCoverage)();
336
+ }
337
+ catch {
338
+ // Ignore errors in background refresh
339
+ }
340
+ }, 1000);
341
+ // Only watch for new/deleted .nornapi files (new swagger URLs)
342
+ nornapiFileWatcher.onDidCreate(() => scheduleNornapiRefresh());
343
+ nornapiFileWatcher.onDidDelete(() => scheduleNornapiRefresh());
344
+ // Watch for .norn and .nornapi file saves to update coverage
345
+ const scheduleCoverageUpdate = debounce(async () => {
346
+ (0, coverageCalculator_1.clearCoverageCache)();
347
+ try {
348
+ await (0, coverageCalculator_1.getCoverage)();
349
+ }
350
+ catch {
351
+ // Ignore errors in background refresh
352
+ }
353
+ }, 500);
354
+ const documentSaveWatcher = vscode.workspace.onDidSaveTextDocument((document) => {
355
+ if (document.languageId === 'norn' || document.languageId === 'nornapi') {
356
+ scheduleCoverageUpdate();
357
+ }
358
+ });
359
+ // Initial coverage calculation (delayed to not slow down activation)
360
+ setTimeout(async () => {
361
+ try {
362
+ await (0, coverageCalculator_1.getCoverage)();
363
+ }
364
+ catch {
365
+ // Ignore errors in initial coverage calculation
366
+ }
367
+ }, 2000);
368
+ // Create status bar item for environment
369
+ const statusBarItem = (0, environmentProvider_1.createStatusBarItem)();
370
+ // Register the CodeLens provider for .norn and .nornapi files
371
+ const codeLensProvider = vscode.languages.registerCodeLensProvider([
372
+ { language: 'norn', scheme: 'file' },
373
+ { language: 'nornapi', scheme: 'file' }
374
+ ], new codeLensProvider_1.HttpCodeLensProvider());
375
+ // Register the Completion provider for IntelliSense
376
+ const completionProvider = vscode.languages.registerCompletionItemProvider([
377
+ { language: 'norn', scheme: 'file' },
378
+ { language: 'nornapi', scheme: 'file' }
379
+ ], new completionProvider_1.HttpCompletionProvider(),
380
+ // Trigger characters:
381
+ // - '{' for variable references {{
382
+ // - '.' for property access ($N.body, user.body)
383
+ // - ' ' for after keywords (print, run, etc.)
384
+ // - '(' and ',' for parameter suggestions
385
+ // - '@' for sequence tags
386
+ // - ':' for header values (Content-Type: ...)
387
+ // - Letters for typing variable names inside () or after print
388
+ '{', '.', ' ', '(', ',', '@', ':', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '_');
389
+ // Register the Diagnostic provider for undefined variables
390
+ const diagnosticProvider = new diagnosticProvider_1.DiagnosticProvider();
391
+ // Register command to refresh diagnostics (called when environment changes)
392
+ const refreshDiagnosticsCommand = vscode.commands.registerCommand('norn.refreshDiagnostics', () => {
393
+ vscode.workspace.textDocuments.forEach((document) => {
394
+ if (document.languageId === 'norn' || document.languageId === 'nornenv' || document.languageId === 'nornapi') {
395
+ diagnosticProvider.updateDiagnostics(document);
396
+ }
397
+ });
398
+ });
399
+ // Register command to open Contract View for schema validation errors
400
+ const openContractViewCommand = vscode.commands.registerCommand('norn.openContractView', (data) => {
401
+ responsePanel_1.ResponsePanel.showContractView(context.extensionUri, data);
402
+ });
403
+ // Register command to open a schema file from CodeLens
404
+ const openSchemaFileCommand = vscode.commands.registerCommand('norn.openSchemaFile', async (schemaPath, sourceFile) => {
405
+ try {
406
+ // Get workspace root for @/ alias resolution
407
+ const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
408
+ const sourceDir = path.dirname(sourceFile);
409
+ // Resolve the schema path (handles @/ alias for contracts folder)
410
+ const resolvedPath = (0, schemaGenerator_1.resolveSchemaPath)(schemaPath, sourceDir, workspaceRoot);
411
+ // Open the schema file
412
+ const uri = vscode.Uri.file(resolvedPath);
413
+ await vscode.commands.executeCommand('vscode.open', uri);
414
+ }
415
+ catch (error) {
416
+ vscode.window.showErrorMessage(`Could not open schema file: ${schemaPath}`);
417
+ }
418
+ });
419
+ // Register command to validate a schema assertion immediately
420
+ const validateSchemaAssertionNowCommand = vscode.commands.registerCommand('norn.validateSchemaAssertionNow', async (line, sourceFile) => {
421
+ try {
422
+ // Find the enclosing sequence and run it
423
+ const editor = vscode.window.activeTextEditor;
424
+ if (!editor || editor.document.uri.fsPath !== sourceFile) {
425
+ // Open the file if not already open
426
+ const doc = await vscode.workspace.openTextDocument(sourceFile);
427
+ await vscode.window.showTextDocument(doc);
428
+ }
429
+ // Run the sequence at this line
430
+ await vscode.commands.executeCommand('norn.runSequence', line);
431
+ }
432
+ catch (error) {
433
+ vscode.window.showErrorMessage(`Validation failed: ${error}`);
434
+ }
435
+ });
436
+ // Register command to view contract validation report
437
+ const viewContractReportCommand = vscode.commands.registerCommand('norn.viewContractReport', async (line, sourceFile, schemaPath) => {
438
+ try {
439
+ const editor = vscode.window.activeTextEditor;
440
+ if (!editor || editor.document.uri.fsPath !== sourceFile) {
441
+ const doc = await vscode.workspace.openTextDocument(sourceFile);
442
+ await vscode.window.showTextDocument(doc);
443
+ }
444
+ const activeEditor = vscode.window.activeTextEditor;
445
+ if (!activeEditor) {
446
+ vscode.window.showErrorMessage('No active editor');
447
+ return;
448
+ }
449
+ // Run the sequence and capture the result
450
+ const fullText = activeEditor.document.getText();
451
+ const workingDir = path.dirname(activeEditor.document.uri.fsPath);
452
+ const sequence = (0, sequenceRunner_1.getSequenceAtLine)(fullText, line);
453
+ if (!sequence) {
454
+ vscode.window.showErrorMessage('No sequence found at this line');
455
+ return;
456
+ }
457
+ // Get variables and run imports
458
+ const fileLevelVariables = (0, parser_1.extractFileLevelVariables)(fullText);
459
+ const envVars = (0, environmentProvider_1.getEnvironmentVariables)();
460
+ const allVariables = { ...envVars, ...fileLevelVariables };
461
+ const importResult = await (0, parser_1.resolveImports)(fullText, workingDir, async (filePath) => {
462
+ const content = await fs.readFile(filePath, 'utf-8');
463
+ return content;
464
+ });
465
+ const fullTextWithImports = importResult.importedContent
466
+ ? `${importResult.importedContent}\n\n${fullText}`
467
+ : fullText;
468
+ const apiDefinitions = (importResult.headerGroups && importResult.headerGroups.length > 0) ||
469
+ (importResult.endpoints && importResult.endpoints.length > 0)
470
+ ? { headerGroups: importResult.headerGroups || [], endpoints: importResult.endpoints || [] }
471
+ : undefined;
472
+ // Run the sequence
473
+ const result = await (0, sequenceRunner_1.runSequence)(sequence.content, allVariables, workingDir, fullTextWithImports, undefined, // no progress callback
474
+ undefined, // callStack
475
+ undefined, // defaultArgs
476
+ apiDefinitions, undefined, // tagFilterOptions
477
+ importResult.sequenceSources // track source files for script path resolution
478
+ );
479
+ // Find the matchesSchema assertion step that corresponds to this line
480
+ const assertionLineRelative = line - sequence.startLine - 1;
481
+ let matchingStep = result.steps.find(s => s.type === 'assertion' &&
482
+ s.assertion?.operator === 'matchesSchema' &&
483
+ s.lineNumber !== undefined &&
484
+ Math.abs(s.lineNumber - assertionLineRelative) <= 1);
485
+ // If not found by line, try to find any matchesSchema assertion with matching schema path
486
+ if (!matchingStep) {
487
+ matchingStep = result.steps.find(s => s.type === 'assertion' &&
488
+ s.assertion?.operator === 'matchesSchema' &&
489
+ (s.assertion?.schemaPath?.includes(schemaPath) || s.assertion?.rightValue?.includes(schemaPath)));
490
+ }
491
+ if (!matchingStep || !matchingStep.assertion) {
492
+ vscode.window.showErrorMessage('Could not find the schema assertion result. Try running the full sequence first.');
493
+ return;
494
+ }
495
+ // Get the response body used in the assertion
496
+ const responseBody = matchingStep.assertion.leftValue;
497
+ const schema = matchingStep.assertion.schema;
498
+ const errors = matchingStep.assertion.schemaErrors || [];
499
+ const resolvedSchemaPath = matchingStep.assertion.schemaPath || schemaPath;
500
+ // Show the Contract Report
501
+ responsePanel_1.ResponsePanel.showContractReport(context.extensionUri, {
502
+ schemaPath: resolvedSchemaPath,
503
+ errors,
504
+ responseBody,
505
+ schema,
506
+ assertionLine: line,
507
+ sourceFile
508
+ });
509
+ // Save validation results and refresh decorations
510
+ saveSchemaValidationResults(result, activeEditor.document.uri.fsPath, sequence.startLine + 1);
511
+ }
512
+ catch (error) {
513
+ vscode.window.showErrorMessage(`Failed to generate contract report: ${error}`);
514
+ }
515
+ });
516
+ // Register the Contract Decoration provider for schema assertion status indicators
517
+ const contractDecorationProvider = new contractDecorationProvider_1.ContractDecorationProvider();
518
+ contractDecorationProviderInstance = contractDecorationProvider;
519
+ // Register hover provider for schema assertion lines
520
+ const contractHoverProvider = vscode.languages.registerHoverProvider({ language: 'norn' }, (0, contractDecorationProvider_1.createContractHoverProvider)());
521
+ // Update diagnostics when document changes
522
+ const onDidChangeDocument = vscode.workspace.onDidChangeTextDocument((event) => {
523
+ if (event.document.languageId === 'norn' || event.document.languageId === 'nornenv' || event.document.languageId === 'nornapi') {
524
+ diagnosticProvider.updateDiagnostics(event.document);
525
+ }
526
+ });
527
+ // Update diagnostics when document opens
528
+ const onDidOpenDocument = vscode.workspace.onDidOpenTextDocument((document) => {
529
+ if (document.languageId === 'norn' || document.languageId === 'nornenv' || document.languageId === 'nornapi') {
530
+ diagnosticProvider.updateDiagnostics(document);
531
+ }
532
+ });
533
+ // Clear diagnostics when document closes
534
+ const onDidCloseDocument = vscode.workspace.onDidCloseTextDocument((document) => {
535
+ diagnosticProvider.clearDiagnostics(document);
536
+ });
537
+ // Update diagnostics for already open documents
538
+ vscode.workspace.textDocuments.forEach((document) => {
539
+ if (document.languageId === 'norn' || document.languageId === 'nornenv' || document.languageId === 'nornapi') {
540
+ diagnosticProvider.updateDiagnostics(document);
541
+ }
542
+ });
543
+ context.subscriptions.push(sendRequestCommand, runSequenceCommand, clearCookiesCommand, showCookiesCommand, selectEnvironmentCommand, selectEnvironmentAndRefreshCommand, createEnvFileCommand, importSwaggerCommand, generateSchemasFromSwaggerCommand, refreshDiagnosticsCommand, openContractViewCommand, openSchemaFileCommand, validateSchemaAssertionNowCommand, viewContractReportCommand, contractDecorationProvider, contractHoverProvider, showCoverageCommand, refreshCoverageCommand, coverageUpdateListener, statusBarItem, coverageStatusBarItem, codeLensProvider, completionProvider, diagnosticProvider, onDidChangeDocument, onDidOpenDocument, onDidCloseDocument, nornapiFileWatcher, documentSaveWatcher, { dispose: environmentProvider_1.disposeStatusBar });
544
+ }
545
+ function formatSequenceCaseLabel(caseParams, index) {
546
+ const entries = Object.entries(caseParams);
547
+ if (entries.length === 0) {
548
+ return `Case ${index + 1}`;
549
+ }
550
+ const formatted = entries.map(([key, value]) => {
551
+ if (typeof value === 'string') {
552
+ return `${key}="${value}"`;
553
+ }
554
+ return `${key}=${value}`;
555
+ });
556
+ return `[${formatted.join(', ')}]`;
557
+ }
558
+ function normalizeTheoryCase(caseValue, sequenceParamNames) {
559
+ if (caseValue && typeof caseValue === 'object' && !Array.isArray(caseValue)) {
560
+ return caseValue;
561
+ }
562
+ if (sequenceParamNames.length === 1) {
563
+ return { [sequenceParamNames[0]]: caseValue };
564
+ }
565
+ return null;
566
+ }
567
+ async function loadTheoryCasesFromSource(sourcePath, workingDir, sequence) {
568
+ const resolvedPath = path.resolve(workingDir, sourcePath);
569
+ const content = await fs.readFile(resolvedPath, 'utf-8');
570
+ const parsed = JSON.parse(content);
571
+ if (!Array.isArray(parsed)) {
572
+ throw new Error(`Theory file "${sourcePath}" must contain a JSON array of test cases`);
573
+ }
574
+ const paramNames = sequence.parameters.map(p => p.name);
575
+ const cases = [];
576
+ for (let i = 0; i < parsed.length; i++) {
577
+ const normalized = normalizeTheoryCase(parsed[i], paramNames);
578
+ if (!normalized) {
579
+ throw new Error(`Theory file "${sourcePath}" has invalid case at index ${i}. ` +
580
+ `Use objects for multi-parameter sequences.`);
581
+ }
582
+ cases.push(normalized);
583
+ }
584
+ return cases;
585
+ }
586
+ async function resolveSequenceRunCases(sequence, workingDir, defaultArgs) {
587
+ const inlineCases = sequence.theoryData?.cases || [];
588
+ let finalCases = [];
589
+ if (inlineCases.length > 0) {
590
+ finalCases = inlineCases;
591
+ }
592
+ else if (sequence.theoryData?.source) {
593
+ finalCases = await loadTheoryCasesFromSource(sequence.theoryData.source, workingDir, sequence);
594
+ }
595
+ if (finalCases.length === 0) {
596
+ return [{ args: { ...defaultArgs } }];
597
+ }
598
+ return finalCases.map((caseParams, index) => {
599
+ const args = { ...defaultArgs };
600
+ for (const [key, value] of Object.entries(caseParams)) {
601
+ if (value !== undefined) {
602
+ args[key] = String(value);
603
+ }
604
+ }
605
+ return {
606
+ label: formatSequenceCaseLabel(caseParams, index),
607
+ args
608
+ };
609
+ });
610
+ }
611
+ async function processSequence(extensionUri, lineFromCodeLens) {
612
+ const editor = vscode.window.activeTextEditor;
613
+ if (!editor) {
614
+ vscode.window.showErrorMessage("No active editor");
615
+ return;
616
+ }
617
+ const fullText = editor.document.getText();
618
+ const targetLine = lineFromCodeLens ?? editor.selection.active.line;
619
+ const sequence = (0, sequenceRunner_1.getSequenceAtLine)(fullText, targetLine);
620
+ if (!sequence) {
621
+ vscode.window.showErrorMessage("No sequence found at cursor position");
622
+ return;
623
+ }
624
+ // Merge environment variables with file-level variables (outside sequences)
625
+ // Variables inside sequences are local to that sequence
626
+ const envVariables = (0, environmentProvider_1.getEnvironmentVariables)();
627
+ const fileVariables = (0, parser_1.extractFileLevelVariables)(fullText);
628
+ const allVariables = { ...envVariables, ...fileVariables };
629
+ // Apply default parameter values for sequences with all-optional parameters
630
+ const defaultArgs = {};
631
+ if (sequence.parameters) {
632
+ for (const param of sequence.parameters) {
633
+ if (param.defaultValue !== undefined) {
634
+ defaultArgs[param.name] = param.defaultValue;
635
+ }
636
+ }
637
+ }
638
+ // Get working directory from the file's location
639
+ const workingDir = path.dirname(editor.document.uri.fsPath);
640
+ // Resolve imports to get the full document text with imported requests/sequences
641
+ const importResult = await (0, parser_1.resolveImports)(fullText, workingDir, async (filePath) => {
642
+ const content = await fs.readFile(filePath, 'utf-8');
643
+ return content;
644
+ });
645
+ // Show import errors - duplicate errors block execution, others are warnings
646
+ const duplicateErrors = importResult.errors.filter(err => err.error.includes('Duplicate header group') ||
647
+ err.error.includes('Duplicate endpoint') ||
648
+ err.error.includes('Duplicate named request') ||
649
+ err.error.includes('Duplicate sequence'));
650
+ const otherErrors = importResult.errors.filter(err => !err.error.includes('Duplicate header group') &&
651
+ !err.error.includes('Duplicate endpoint') &&
652
+ !err.error.includes('Duplicate named request') &&
653
+ !err.error.includes('Duplicate sequence'));
654
+ // Show non-duplicate errors as warnings
655
+ for (const err of otherErrors) {
656
+ vscode.window.showWarningMessage(`Import error: ${err.path} - ${err.error}`);
657
+ }
658
+ // Duplicate errors block execution
659
+ if (duplicateErrors.length > 0) {
660
+ vscode.window.showErrorMessage(`Cannot run sequence - duplicate definitions found:\n${duplicateErrors[0].error}`);
661
+ return;
662
+ }
663
+ // Combine original text with imported content
664
+ const fullTextWithImports = importResult.importedContent
665
+ ? `${importResult.importedContent}\n\n${fullText}`
666
+ : fullText;
667
+ // Build API definitions from imports (header groups and endpoints from .nornapi files)
668
+ const apiDefinitions = (importResult.headerGroups && importResult.headerGroups.length > 0) ||
669
+ (importResult.endpoints && importResult.endpoints.length > 0)
670
+ ? { headerGroups: importResult.headerGroups || [], endpoints: importResult.endpoints || [] }
671
+ : undefined;
672
+ let executedRuns = false;
673
+ try {
674
+ const runCases = await resolveSequenceRunCases(sequence, workingDir, defaultArgs);
675
+ if (runCases.length <= 1) {
676
+ // Single run: keep streaming behavior.
677
+ const totalSteps = (0, sequenceRunner_1.countSequenceSteps)(sequence.content);
678
+ responsePanel_1.ResponsePanel.startSequenceRun(extensionUri, sequence.name, totalSteps, editor.document.uri.fsPath);
679
+ try {
680
+ executedRuns = true;
681
+ const runArgs = runCases[0]?.args ?? defaultArgs;
682
+ const result = await (0, sequenceRunner_1.runSequence)(sequence.content, allVariables, workingDir, fullTextWithImports, (progressInfo) => {
683
+ // Handle sequence start/end markers for grouping
684
+ if (progressInfo.stepType === 'sequenceStart' || progressInfo.stepType === 'sequenceEnd') {
685
+ responsePanel_1.ResponsePanel.addSequenceStep(undefined, progressInfo);
686
+ }
687
+ // Stream each step result to the panel as it completes
688
+ else if (progressInfo.stepResult) {
689
+ responsePanel_1.ResponsePanel.addSequenceStep(progressInfo.stepResult, progressInfo);
690
+ }
691
+ }, undefined, // callStack
692
+ runArgs, apiDefinitions, // header groups and endpoints from .nornapi imports
693
+ undefined, // tagFilterOptions
694
+ importResult.sequenceSources // track source files for script path resolution
695
+ );
696
+ result.name = sequence.name;
697
+ responsePanel_1.ResponsePanel.finalizeSequence(result);
698
+ // Save validation results for schema assertions and refresh decorations
699
+ // Add sequence.startLine + 1 to convert relative line numbers to absolute file line numbers
700
+ // (+1 because the sequence content starts after the "sequence ..." declaration line)
701
+ saveSchemaValidationResults(result, editor.document.uri.fsPath, sequence.startLine + 1);
702
+ }
703
+ catch (error) {
704
+ const errorMessage = error instanceof Error ? error.message : String(error);
705
+ vscode.window.showErrorMessage(`Sequence failed: ${errorMessage}`);
706
+ // Finalize with error state
707
+ responsePanel_1.ResponsePanel.finalizeSequence({
708
+ name: sequence.name,
709
+ success: false,
710
+ responses: [],
711
+ scriptResults: [],
712
+ assertionResults: [],
713
+ steps: [],
714
+ errors: [errorMessage],
715
+ duration: 0
716
+ });
717
+ }
718
+ }
719
+ else {
720
+ // Parameterized runs: execute all cases and show combined report in one panel.
721
+ const caseResults = [];
722
+ await vscode.window.withProgress({
723
+ location: vscode.ProgressLocation.Notification,
724
+ title: `Running ${sequence.name} (${runCases.length} cases)`,
725
+ cancellable: false
726
+ }, async (progress) => {
727
+ for (let i = 0; i < runCases.length; i++) {
728
+ const runCase = runCases[i];
729
+ const runLabel = runCase.label || `Case ${i + 1}`;
730
+ progress.report({
731
+ message: runLabel,
732
+ increment: 100 / runCases.length
733
+ });
734
+ try {
735
+ executedRuns = true;
736
+ const result = await (0, sequenceRunner_1.runSequence)(sequence.content, allVariables, workingDir, fullTextWithImports, undefined, // no per-step streaming in multi-run mode
737
+ undefined, // callStack
738
+ runCase.args, apiDefinitions, undefined, importResult.sequenceSources);
739
+ result.name = sequence.name;
740
+ saveSchemaValidationResults(result, editor.document.uri.fsPath, sequence.startLine + 1);
741
+ caseResults.push({ label: runLabel, result });
742
+ }
743
+ catch (error) {
744
+ const errorMessage = error instanceof Error ? error.message : String(error);
745
+ caseResults.push({
746
+ label: runLabel,
747
+ result: {
748
+ name: sequence.name,
749
+ success: false,
750
+ responses: [],
751
+ scriptResults: [],
752
+ assertionResults: [],
753
+ steps: [],
754
+ errors: [errorMessage],
755
+ duration: 0
756
+ }
757
+ });
758
+ }
759
+ }
760
+ });
761
+ responsePanel_1.ResponsePanel.showMultiSequenceResult(extensionUri, sequence.name, caseResults);
762
+ }
763
+ if (executedRuns) {
764
+ // Recalculate coverage after sequence execution (uses cached swagger specs)
765
+ (0, coverageCalculator_1.recalculateCoverageAfterExecution)().catch(() => {
766
+ // Silently ignore coverage calculation errors
767
+ });
768
+ }
769
+ }
770
+ catch (error) {
771
+ const errorMessage = error instanceof Error ? error.message : String(error);
772
+ vscode.window.showErrorMessage(`Sequence failed: ${errorMessage}`);
773
+ }
774
+ }
775
+ /**
776
+ * Save schema validation results to cache and refresh decorations
777
+ */
778
+ function saveSchemaValidationResults(result, sourceFile, lineOffset = 0) {
779
+ try {
780
+ // Find all matchesSchema assertion steps
781
+ for (const step of result.steps) {
782
+ if (step.type === 'assertion' && step.assertion?.operator === 'matchesSchema') {
783
+ const assertion = step.assertion;
784
+ const schemaPath = assertion.schemaPath || assertion.rightValue || '';
785
+ const status = assertion.passed ? 'pass' : 'fail';
786
+ const errorCount = assertion.schemaErrors?.length || 0;
787
+ const errorSummary = assertion.schemaErrors?.[0]?.message || assertion.error?.substring(0, 100);
788
+ // Only cache if we have a line number
789
+ if (step.lineNumber !== undefined) {
790
+ // Convert relative line number to absolute file line number using lineOffset
791
+ const absoluteLine = step.lineNumber + lineOffset;
792
+ (0, validationCache_1.saveValidationResult)({
793
+ schemaPath,
794
+ sourceFile,
795
+ assertionLine: absoluteLine,
796
+ status,
797
+ lastRunTime: new Date().toISOString(),
798
+ errorCount: status === 'fail' ? errorCount : undefined,
799
+ errorSummary: status === 'fail' ? errorSummary : undefined
800
+ });
801
+ }
802
+ }
803
+ }
804
+ // Refresh decorations
805
+ contractDecorationProviderInstance?.refreshAllDecorations();
806
+ }
807
+ catch (e) {
808
+ // Silently ignore cache errors
809
+ console.error('Failed to save validation results:', e);
810
+ }
811
+ }
812
+ async function processEditorsInput(extensionUri, lineFromCodeLens) {
813
+ const editor = vscode.window.activeTextEditor;
814
+ if (!editor) {
815
+ vscode.window.showErrorMessage("No active editor");
816
+ return;
817
+ }
818
+ const fullText = editor.document.getText();
819
+ const selection = editor.selection;
820
+ let requestText;
821
+ if (!selection.isEmpty) {
822
+ // User selected specific text
823
+ requestText = editor.document.getText(selection);
824
+ }
825
+ else {
826
+ // Use line from CodeLens if provided, otherwise use cursor position
827
+ const targetLine = lineFromCodeLens ?? selection.active.line;
828
+ requestText = (0, parser_1.getRequestBlockAtLine)(fullText, targetLine);
829
+ }
830
+ // Get working directory from the file's location
831
+ const workingDir = path.dirname(editor.document.uri.fsPath);
832
+ try {
833
+ // Extract variables: environment + file (file takes precedence)
834
+ const envVariables = (0, environmentProvider_1.getEnvironmentVariables)();
835
+ const fileVariables = (0, parser_1.extractVariables)(fullText);
836
+ const variables = { ...envVariables, ...fileVariables };
837
+ // Resolve imports to get API definitions (endpoints and header groups)
838
+ const importResult = await (0, parser_1.resolveImports)(fullText, workingDir, async (filePath) => {
839
+ const content = await fs.readFile(filePath, 'utf-8');
840
+ return content;
841
+ });
842
+ // Check for duplicate definition errors which should block execution
843
+ const duplicateErrors = importResult.errors.filter(err => err.error.includes('Duplicate header group') ||
844
+ err.error.includes('Duplicate endpoint') ||
845
+ err.error.includes('Duplicate named request') ||
846
+ err.error.includes('Duplicate sequence'));
847
+ if (duplicateErrors.length > 0) {
848
+ vscode.window.showErrorMessage(`Cannot run request - duplicate definitions found:\n${duplicateErrors[0].error}`);
849
+ return;
850
+ }
851
+ // Build API definitions from imports
852
+ const apiDefinitions = (importResult.headerGroups && importResult.headerGroups.length > 0) ||
853
+ (importResult.endpoints && importResult.endpoints.length > 0)
854
+ ? { headerGroups: importResult.headerGroups || [], endpoints: importResult.endpoints || [] }
855
+ : undefined;
856
+ let parsed;
857
+ let requestDescription;
858
+ // Check if this is an API request (uses endpoint syntax like GET GetPetById(1))
859
+ if (apiDefinitions &&
860
+ apiDefinitions.endpoints.length > 0 &&
861
+ (0, nornapiParser_1.isApiRequestLine)(requestText, apiDefinitions.endpoints)) {
862
+ const apiRequest = (0, nornapiParser_1.parseApiRequest)(requestText, apiDefinitions.endpoints, apiDefinitions.headerGroups);
863
+ if (apiRequest) {
864
+ const endpoint = (0, nornapiParser_1.getEndpoint)({ headerGroups: apiDefinitions.headerGroups, endpoints: apiDefinitions.endpoints }, apiRequest.endpointName);
865
+ if (endpoint) {
866
+ // Substitute {{variable}} references in the endpoint path
867
+ let resolvedPath = endpoint.path;
868
+ for (const [key, value] of Object.entries(variables)) {
869
+ resolvedPath = resolvedPath.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), String(value));
870
+ }
871
+ // Substitute {param} placeholders with the values passed to the endpoint
872
+ for (const [paramName, paramValue] of Object.entries(apiRequest.params)) {
873
+ // Resolve variable references in param values
874
+ let resolvedValue = paramValue;
875
+ if (variables[paramValue] !== undefined) {
876
+ resolvedValue = String(variables[paramValue]);
877
+ }
878
+ else if (paramValue.startsWith('{{') && paramValue.endsWith('}}')) {
879
+ const varName = paramValue.slice(2, -2);
880
+ if (variables[varName] !== undefined) {
881
+ resolvedValue = String(variables[varName]);
882
+ }
883
+ }
884
+ resolvedPath = resolvedPath.replace(`{${paramName}}`, resolvedValue);
885
+ }
886
+ // Collect headers from header groups
887
+ const combinedHeaders = {};
888
+ for (const groupName of apiRequest.headerGroupNames) {
889
+ const group = apiDefinitions.headerGroups.find(hg => hg.name === groupName);
890
+ if (group) {
891
+ const resolvedHeaders = (0, nornapiParser_1.resolveHeaderValues)(group, variables);
892
+ Object.assign(combinedHeaders, resolvedHeaders);
893
+ }
894
+ }
895
+ // Add inline headers (these take precedence over header groups)
896
+ for (const [headerName, headerValue] of Object.entries(apiRequest.inlineHeaders)) {
897
+ // Resolve variable references in header values
898
+ let resolved = headerValue;
899
+ resolved = resolved.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (_, varName) => {
900
+ return variables[varName] !== undefined ? String(variables[varName]) : `{{${varName}}}`;
901
+ });
902
+ combinedHeaders[headerName] = resolved;
903
+ }
904
+ // Parse the request text to extract body (if any)
905
+ const parsedForBody = (0, parser_1.parserHttpRequest)(requestText, variables);
906
+ parsed = {
907
+ method: apiRequest.method,
908
+ url: resolvedPath,
909
+ headers: combinedHeaders,
910
+ body: parsedForBody.body
911
+ };
912
+ requestDescription = `${apiRequest.endpointName}(${Object.values(apiRequest.params).join(', ')}) → ${parsed.method} ${parsed.url}`;
913
+ }
914
+ else {
915
+ throw new Error(`Unknown endpoint: ${apiRequest.endpointName}`);
916
+ }
917
+ }
918
+ else {
919
+ // Fallback to regular parsing
920
+ parsed = (0, parser_1.parserHttpRequest)(requestText, variables);
921
+ requestDescription = `${parsed.method} ${parsed.url}`;
922
+ }
923
+ }
924
+ else {
925
+ // Regular HTTP request
926
+ parsed = (0, parser_1.parserHttpRequest)(requestText, variables);
927
+ requestDescription = `${parsed.method} ${parsed.url}`;
928
+ // Check for header group names in the request text
929
+ // This allows: GET https://example.com Json Auth
930
+ // Or: GET https://example.com
931
+ // Json
932
+ // Auth
933
+ if (apiDefinitions && apiDefinitions.headerGroups.length > 0) {
934
+ const requestLines = requestText.split('\n');
935
+ const headerGroupNames = apiDefinitions.headerGroups.map(hg => hg.name);
936
+ const foundGroups = [];
937
+ for (const line of requestLines) {
938
+ const trimmed = line.trim();
939
+ // Skip empty lines, comments, variable lines
940
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('var ')) {
941
+ continue;
942
+ }
943
+ // Check if this is the HTTP method line - look for header groups at the end
944
+ const methodMatch = trimmed.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(.+)$/i);
945
+ if (methodMatch) {
946
+ const afterMethod = methodMatch[2];
947
+ const tokens = afterMethod.split(/\s+/);
948
+ // Scan from the end to find header groups
949
+ for (let i = tokens.length - 1; i >= 0; i--) {
950
+ if (headerGroupNames.includes(tokens[i])) {
951
+ foundGroups.push(tokens[i]);
952
+ }
953
+ else {
954
+ break;
955
+ }
956
+ }
957
+ continue;
958
+ }
959
+ // Skip lines that look like headers (Name: Value)
960
+ if (/^[A-Za-z0-9\-_]+\s*:\s*.+$/.test(trimmed)) {
961
+ continue;
962
+ }
963
+ // Check if this line contains header group names (can be multiple space-separated)
964
+ const potentialGroups = trimmed.split(/\s+/);
965
+ for (const groupName of potentialGroups) {
966
+ if (headerGroupNames.includes(groupName)) {
967
+ foundGroups.push(groupName);
968
+ }
969
+ }
970
+ }
971
+ // Collect headers from all found groups and strip from URL
972
+ for (const groupName of foundGroups) {
973
+ const group = apiDefinitions.headerGroups.find(hg => hg.name === groupName);
974
+ if (group) {
975
+ const resolvedHeaders = (0, nornapiParser_1.resolveHeaderValues)(group, variables);
976
+ parsed.headers = { ...resolvedHeaders, ...parsed.headers };
977
+ }
978
+ // Strip from URL
979
+ const endPattern = new RegExp(`\\s+${groupName}$`);
980
+ parsed.url = parsed.url.replace(endPattern, '');
981
+ }
982
+ parsed.url = parsed.url.trim();
983
+ requestDescription = `${parsed.method} ${parsed.url}`;
984
+ }
985
+ }
986
+ // Show loading indicator
987
+ await vscode.window.withProgress({
988
+ location: vscode.ProgressLocation.Notification,
989
+ title: `Sending ${parsed.method} request...`,
990
+ cancellable: false
991
+ }, async () => {
992
+ try {
993
+ const response = await (0, httpClient_1.sendRequest)(parsed);
994
+ responsePanel_1.ResponsePanel.show(extensionUri, parsed.method, parsed.url, response);
995
+ }
996
+ catch (error) {
997
+ responsePanel_1.ResponsePanel.showError(extensionUri, parsed.method, parsed.url, error.message);
998
+ }
999
+ });
1000
+ }
1001
+ catch (error) {
1002
+ vscode.window.showErrorMessage(error.message);
1003
+ }
1004
+ }
1005
+ /**
1006
+ * Simple debounce helper
1007
+ */
1008
+ function debounce(fn, delay) {
1009
+ let timeoutId;
1010
+ return ((...args) => {
1011
+ if (timeoutId) {
1012
+ clearTimeout(timeoutId);
1013
+ }
1014
+ timeoutId = setTimeout(() => {
1015
+ fn(...args);
1016
+ timeoutId = undefined;
1017
+ }, delay);
1018
+ });
1019
+ }
1020
+ function deactivate() {
1021
+ // Clear caches on deactivation
1022
+ (0, swaggerParser_1.clearSwaggerCache)();
1023
+ (0, coverageCalculator_1.clearCoverageCache)();
1024
+ }
1025
+ //# sourceMappingURL=extension.js.map