norn-cli 1.4.0 → 1.4.2

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.
@@ -100,6 +100,46 @@ function stripInlineComment(line) {
100
100
  }
101
101
  return line;
102
102
  }
103
+ function isHttpRequestStart(line) {
104
+ return /^(?:var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*)?(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+/i.test(line.trim());
105
+ }
106
+ function isRequestBodyBoundary(line) {
107
+ const trimmed = line.trim();
108
+ if (!trimmed) {
109
+ return false;
110
+ }
111
+ return isHttpRequestStart(trimmed) ||
112
+ /^\[/.test(trimmed) ||
113
+ /^(?:test\s+)?sequence\s+/i.test(trimmed) ||
114
+ /^end\s+sequence$/i.test(trimmed) ||
115
+ /^(?:var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*)?run\s+/i.test(trimmed) ||
116
+ /^assert\s+/i.test(trimmed) ||
117
+ /^print\s+/i.test(trimmed) ||
118
+ /^if\s+/i.test(trimmed) ||
119
+ /^foreach\s+/i.test(trimmed) ||
120
+ /^return\s+/i.test(trimmed);
121
+ }
122
+ function countChar(value, char) {
123
+ let count = 0;
124
+ for (let i = 0; i < value.length; i++) {
125
+ if (value[i] === char) {
126
+ count++;
127
+ }
128
+ }
129
+ return count;
130
+ }
131
+ function looksLikeJsonPropertyLine(line) {
132
+ return /^"[^"\\]*(?:\\.[^"\\]*)*"\s*:\s*.+$/.test(line.trim());
133
+ }
134
+ function getHeaderValueCaseInsensitive(headers, headerName) {
135
+ const target = headerName.toLowerCase();
136
+ for (const [name, value] of Object.entries(headers)) {
137
+ if (name.toLowerCase() === target) {
138
+ return value;
139
+ }
140
+ }
141
+ return undefined;
142
+ }
103
143
  /**
104
144
  * Tokenize print statement content into individual tokens.
105
145
  * Respects quoted strings and splits on operators/whitespace.
@@ -181,7 +221,7 @@ class DiagnosticProvider {
181
221
  const text = document.getText();
182
222
  // Extract file-level variables (outside sequences) + environment
183
223
  const fileLevelVariables = (0, parser_1.extractFileLevelVariables)(text);
184
- const envVariables = (0, environmentProvider_1.getEnvironmentVariables)();
224
+ const envVariables = (0, environmentProvider_1.getEnvironmentVariables)(document.uri.fsPath);
185
225
  const globalVariables = { ...envVariables, ...fileLevelVariables };
186
226
  // Extract sequences for scope-aware checking
187
227
  const sequences = (0, sequenceRunner_1.extractSequences)(text);
@@ -341,7 +381,11 @@ class DiagnosticProvider {
341
381
  diagnostics.push(diagnostic);
342
382
  }
343
383
  else {
344
- importedHeaderGroups.set(group.name, { sourcePath: imp.path, lineNumber: imp.lineNumber });
384
+ importedHeaderGroups.set(group.name, {
385
+ sourcePath: imp.path,
386
+ lineNumber: imp.lineNumber,
387
+ headerGroup: group
388
+ });
345
389
  }
346
390
  }
347
391
  }
@@ -585,6 +629,95 @@ class DiagnosticProvider {
585
629
  }
586
630
  }
587
631
  }
632
+ // Check for JSON-like request body properties without surrounding { } object
633
+ const bodyMethods = new Set(['POST', 'PUT', 'PATCH']);
634
+ const requestStartRegex = /^\s*(?:var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*)?(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+/i;
635
+ const inlineHeaderRegex = /^([A-Za-z][A-Za-z0-9\-]*)\s*:\s*(.+)$/;
636
+ const formContentTypes = ['application/x-www-form-urlencoded', 'multipart/form-data'];
637
+ for (let i = 0; i < lines.length; i++) {
638
+ const line = lines[i];
639
+ if (isCommentLine(line)) {
640
+ continue;
641
+ }
642
+ const lineWithoutComment = stripInlineComment(line).trim();
643
+ const requestMatch = lineWithoutComment.match(requestStartRegex);
644
+ if (!requestMatch) {
645
+ continue;
646
+ }
647
+ const method = requestMatch[1].toUpperCase();
648
+ if (!bodyMethods.has(method)) {
649
+ continue;
650
+ }
651
+ let requestContentType;
652
+ const afterMethod = lineWithoutComment.replace(requestStartRegex, '').trim();
653
+ const methodLineTokens = afterMethod.split(/\s+/).filter(t => t.length > 0);
654
+ for (const token of methodLineTokens) {
655
+ const cleanToken = token.replace(/[,)]+$/g, '');
656
+ const group = importedHeaderGroups.get(cleanToken)?.headerGroup;
657
+ if (group) {
658
+ const headerValue = getHeaderValueCaseInsensitive(group.headers, 'Content-Type');
659
+ if (headerValue) {
660
+ requestContentType = headerValue.toLowerCase();
661
+ }
662
+ }
663
+ }
664
+ let objectDepth = 0;
665
+ let hasReported = false;
666
+ let reachedBody = false;
667
+ for (let j = i + 1; j < lines.length; j++) {
668
+ const bodyLine = lines[j];
669
+ if (isCommentLine(bodyLine)) {
670
+ continue;
671
+ }
672
+ const bodyLineWithoutComment = stripInlineComment(bodyLine);
673
+ const trimmedBodyLine = bodyLineWithoutComment.trim();
674
+ if (!trimmedBodyLine) {
675
+ continue;
676
+ }
677
+ if (isRequestBodyBoundary(trimmedBodyLine)) {
678
+ break;
679
+ }
680
+ if (!reachedBody) {
681
+ const headerMatch = trimmedBodyLine.match(inlineHeaderRegex);
682
+ if (headerMatch) {
683
+ if (headerMatch[1].toLowerCase() === 'content-type') {
684
+ requestContentType = headerMatch[2].trim().toLowerCase();
685
+ }
686
+ continue;
687
+ }
688
+ const headerGroup = importedHeaderGroups.get(trimmedBodyLine)?.headerGroup;
689
+ if (headerGroup) {
690
+ const headerValue = getHeaderValueCaseInsensitive(headerGroup.headers, 'Content-Type');
691
+ if (headerValue) {
692
+ requestContentType = headerValue.toLowerCase();
693
+ }
694
+ continue;
695
+ }
696
+ }
697
+ reachedBody = true;
698
+ const isFormBody = requestContentType
699
+ ? formContentTypes.some(ct => requestContentType.includes(ct))
700
+ : false;
701
+ if (isFormBody) {
702
+ continue;
703
+ }
704
+ const startsJsonObject = trimmedBodyLine.startsWith('{');
705
+ if (!hasReported && looksLikeJsonPropertyLine(trimmedBodyLine) && objectDepth === 0 && !startsJsonObject) {
706
+ const startCol = bodyLine.indexOf('"');
707
+ const range = new vscode.Range(new vscode.Position(j, startCol >= 0 ? startCol : 0), new vscode.Position(j, bodyLine.length));
708
+ const diagnostic = new vscode.Diagnostic(range, `Invalid JSON body: properties must be wrapped in '{' and '}'.`, vscode.DiagnosticSeverity.Error);
709
+ diagnostic.source = 'Norn';
710
+ diagnostic.code = 'invalid-json-body-missing-braces';
711
+ diagnostics.push(diagnostic);
712
+ hasReported = true;
713
+ }
714
+ objectDepth += countChar(trimmedBodyLine, '{');
715
+ objectDepth -= countChar(trimmedBodyLine, '}');
716
+ if (objectDepth < 0) {
717
+ objectDepth = 0;
718
+ }
719
+ }
720
+ }
588
721
  // Find empty variable references {{}} or {{ }}
589
722
  const emptyVarRegex = /\{\{\s*\}\}/g;
590
723
  let emptyMatch;
@@ -42,9 +42,11 @@ exports.setActiveEnvironment = setActiveEnvironment;
42
42
  exports.getEnvironmentVariables = getEnvironmentVariables;
43
43
  exports.getAvailableEnvironments = getAvailableEnvironments;
44
44
  exports.createStatusBarItem = createStatusBarItem;
45
+ exports.refreshEnvironmentStatusBar = refreshEnvironmentStatusBar;
45
46
  exports.showEnvironmentPicker = showEnvironmentPicker;
46
47
  exports.disposeStatusBar = disposeStatusBar;
47
48
  exports.createCoverageStatusBarItem = createCoverageStatusBarItem;
49
+ exports.refreshCoverageStatusBarContext = refreshCoverageStatusBarContext;
48
50
  exports.updateCoverageStatusBar = updateCoverageStatusBar;
49
51
  exports.getCoverageStatusBarItem = getCoverageStatusBarItem;
50
52
  const vscode = __importStar(require("vscode"));
@@ -55,6 +57,39 @@ const ENV_FILENAME = '.nornenv';
55
57
  let activeEnvironment;
56
58
  let statusBarItem;
57
59
  let coverageStatusBarItem;
60
+ let hasCoverageData = false;
61
+ /**
62
+ * Resolves where environment file discovery should start.
63
+ * If the path points to a directory, search from that directory.
64
+ * If it points to a file (or non-existent path), search from the parent directory.
65
+ */
66
+ function getEnvSearchStartDirectory(targetPath) {
67
+ const resolvedPath = path.resolve(targetPath);
68
+ try {
69
+ const stats = fs.statSync(resolvedPath);
70
+ return stats.isDirectory() ? resolvedPath : path.dirname(resolvedPath);
71
+ }
72
+ catch {
73
+ return path.dirname(resolvedPath);
74
+ }
75
+ }
76
+ function getContextEnvFile() {
77
+ const activeEditor = vscode.window.activeTextEditor;
78
+ if (activeEditor && activeEditor.document.uri.scheme === 'file') {
79
+ return findEnvFileFromPath(activeEditor.document.uri.fsPath);
80
+ }
81
+ return findEnvFile();
82
+ }
83
+ function resolveEnvFilePath(pathOrSourceFile) {
84
+ if (!pathOrSourceFile) {
85
+ return getContextEnvFile();
86
+ }
87
+ const resolvedPath = path.resolve(pathOrSourceFile);
88
+ if (path.basename(resolvedPath) === ENV_FILENAME && fs.existsSync(resolvedPath)) {
89
+ return resolvedPath;
90
+ }
91
+ return findEnvFileFromPath(resolvedPath);
92
+ }
58
93
  /**
59
94
  * Finds the .nornenv file in the workspace
60
95
  */
@@ -75,14 +110,17 @@ function findEnvFile() {
75
110
  * Finds .nornenv file relative to a specific file path (for CLI usage)
76
111
  */
77
112
  function findEnvFileFromPath(filePath) {
78
- let dir = path.dirname(filePath);
79
- const root = path.parse(dir).root;
80
- while (dir !== root) {
113
+ let dir = getEnvSearchStartDirectory(filePath);
114
+ while (true) {
81
115
  const envPath = path.join(dir, ENV_FILENAME);
82
116
  if (fs.existsSync(envPath)) {
83
117
  return envPath;
84
118
  }
85
- dir = path.dirname(dir);
119
+ const parentDir = path.dirname(dir);
120
+ if (parentDir === dir) {
121
+ break;
122
+ }
123
+ dir = parentDir;
86
124
  }
87
125
  return undefined;
88
126
  }
@@ -150,10 +188,14 @@ function parseEnvFile(content) {
150
188
  return config;
151
189
  }
152
190
  /**
153
- * Reads and parses the .nornenv file
191
+ * Reads and parses the resolved .nornenv file.
192
+ * Accepts either:
193
+ * - a path to a .nornenv file, or
194
+ * - a source file/directory path (it will resolve nearest .nornenv), or
195
+ * - undefined (uses current editor context/workspace fallback).
154
196
  */
155
- function loadEnvironmentConfig(envFilePath) {
156
- const filePath = envFilePath || findEnvFile();
197
+ function loadEnvironmentConfig(pathOrSourceFile) {
198
+ const filePath = resolveEnvFilePath(pathOrSourceFile);
157
199
  if (!filePath || !fs.existsSync(filePath)) {
158
200
  return undefined;
159
201
  }
@@ -181,8 +223,8 @@ function setActiveEnvironment(envName) {
181
223
  /**
182
224
  * Gets all variables for the current environment (common + environment-specific)
183
225
  */
184
- function getEnvironmentVariables(envFilePath) {
185
- const config = loadEnvironmentConfig(envFilePath);
226
+ function getEnvironmentVariables(pathOrSourceFile) {
227
+ const config = loadEnvironmentConfig(pathOrSourceFile);
186
228
  if (!config) {
187
229
  return {};
188
230
  }
@@ -200,8 +242,8 @@ function getEnvironmentVariables(envFilePath) {
200
242
  /**
201
243
  * Gets available environment names
202
244
  */
203
- function getAvailableEnvironments(envFilePath) {
204
- const config = loadEnvironmentConfig(envFilePath);
245
+ function getAvailableEnvironments(pathOrSourceFile) {
246
+ const config = loadEnvironmentConfig(pathOrSourceFile);
205
247
  if (!config) {
206
248
  return [];
207
249
  }
@@ -217,6 +259,12 @@ function createStatusBarItem() {
217
259
  statusBarItem.show();
218
260
  return statusBarItem;
219
261
  }
262
+ /**
263
+ * Refreshes the status bar text based on current editor context.
264
+ */
265
+ function refreshEnvironmentStatusBar() {
266
+ updateStatusBar();
267
+ }
220
268
  /**
221
269
  * Updates the status bar item text
222
270
  */
@@ -224,7 +272,7 @@ function updateStatusBar() {
224
272
  if (!statusBarItem) {
225
273
  return;
226
274
  }
227
- const envFile = findEnvFile();
275
+ const envFile = getContextEnvFile();
228
276
  if (!envFile) {
229
277
  statusBarItem.text = '$(globe) Norn: No Env';
230
278
  statusBarItem.tooltip = 'No .nornenv file found';
@@ -243,7 +291,8 @@ function updateStatusBar() {
243
291
  * Shows the environment picker
244
292
  */
245
293
  async function showEnvironmentPicker() {
246
- const environments = getAvailableEnvironments();
294
+ const envFilePath = getContextEnvFile();
295
+ const environments = getAvailableEnvironments(envFilePath);
247
296
  if (environments.length === 0) {
248
297
  const createFile = await vscode.window.showInformationMessage('No .nornenv file found. Would you like to create one?', 'Create .nornenv', 'Cancel');
249
298
  if (createFile === 'Create .nornenv') {
@@ -338,6 +387,34 @@ function createCoverageStatusBarItem() {
338
387
  coverageStatusBarItem.hide(); // Hidden until we know there's swagger
339
388
  return coverageStatusBarItem;
340
389
  }
390
+ function getActiveNornapiPath() {
391
+ const activeEditor = vscode.window.activeTextEditor;
392
+ if (!activeEditor) {
393
+ return undefined;
394
+ }
395
+ return activeEditor.document.languageId === 'nornapi'
396
+ ? activeEditor.document.uri.fsPath
397
+ : undefined;
398
+ }
399
+ /**
400
+ * Refresh coverage status bar command context for the current active editor.
401
+ */
402
+ function refreshCoverageStatusBarContext() {
403
+ if (!coverageStatusBarItem || !hasCoverageData) {
404
+ return;
405
+ }
406
+ const activeNornapiPath = getActiveNornapiPath();
407
+ coverageStatusBarItem.command = {
408
+ command: 'norn.showCoverage',
409
+ title: 'Show Coverage',
410
+ arguments: activeNornapiPath ? [activeNornapiPath] : []
411
+ };
412
+ coverageStatusBarItem.text = '$(graph) Coverage';
413
+ coverageStatusBarItem.tooltip = activeNornapiPath
414
+ ? 'Show API coverage for this .nornapi file (includes this folder and subfolders only).'
415
+ : 'Show API coverage';
416
+ coverageStatusBarItem.show();
417
+ }
341
418
  /**
342
419
  * Updates the coverage status bar display
343
420
  */
@@ -346,23 +423,15 @@ function updateCoverageStatusBar(hasSwagger, percentage, total, covered) {
346
423
  return;
347
424
  }
348
425
  if (!hasSwagger) {
426
+ hasCoverageData = false;
349
427
  coverageStatusBarItem.hide();
350
428
  return;
351
429
  }
352
- // Choose icon based on coverage level
353
- let icon = '$(graph)';
354
- if (percentage >= 80) {
355
- icon = '$(pass)';
356
- }
357
- else if (percentage >= 50) {
358
- icon = '$(warning)';
359
- }
360
- else if (percentage > 0) {
361
- icon = '$(error)';
362
- }
363
- coverageStatusBarItem.text = `${icon} ${percentage}%`;
364
- coverageStatusBarItem.tooltip = `API Coverage: ${covered}/${total} response codes tested\nClick to view details`;
365
- coverageStatusBarItem.show();
430
+ hasCoverageData = true;
431
+ void percentage;
432
+ void total;
433
+ void covered;
434
+ refreshCoverageStatusBarContext();
366
435
  }
367
436
  /**
368
437
  * Get the coverage status bar item (for external updates)
package/out/extension.js CHANGED
@@ -57,6 +57,8 @@ const compareContentProvider_1 = require("./compareContentProvider");
57
57
  const contractDecorationProvider_1 = require("./contractDecorationProvider");
58
58
  const validationCache_1 = require("./validationCache");
59
59
  const schemaGenerator_1 = require("./schemaGenerator");
60
+ const swaggerBodyIntellisenseCache_1 = require("./swaggerBodyIntellisenseCache");
61
+ const chatParticipant_1 = require("./chatParticipant");
60
62
  // Module-level reference to contract decoration provider for refreshing after sequence runs
61
63
  let contractDecorationProviderInstance;
62
64
  function activate(context) {
@@ -66,6 +68,8 @@ function activate(context) {
66
68
  // Register Test Explorer integration
67
69
  const testController = new testProvider_1.NornTestController();
68
70
  context.subscriptions.push(testController);
71
+ // Register @norn Copilot Chat participant (gracefully skipped if Copilot is not installed)
72
+ (0, chatParticipant_1.registerChatParticipant)(context);
69
73
  const sendRequestCommand = vscode.commands.registerCommand('norn.sendRequest', (lineFromCodeLens) => processEditorsInput(context.extensionUri, lineFromCodeLens));
70
74
  const runSequenceCommand = vscode.commands.registerCommand('norn.runSequence', (lineFromCodeLens) => processSequence(context.extensionUri, lineFromCodeLens));
71
75
  const clearCookiesCommand = vscode.commands.registerCommand('norn.clearCookies', async () => {
@@ -187,6 +191,15 @@ var debug = false
187
191
  if (baseUrlInput === undefined) {
188
192
  return;
189
193
  }
194
+ // Cache request body schemas for IntelliSense (best effort)
195
+ try {
196
+ progress.report({ message: 'Caching request body schemas...' });
197
+ const requestSchemas = await (0, swaggerParser_1.extractRequestBodySchemas)(swaggerUrl);
198
+ (0, swaggerBodyIntellisenseCache_1.saveRequestBodySchemasForUrl)(swaggerUrl, baseUrlInput || spec.baseUrl, requestSchemas);
199
+ }
200
+ catch {
201
+ // Keep endpoint import successful even if schema caching fails
202
+ }
190
203
  // Generate the content
191
204
  const content = (0, swaggerParser_1.generateNornapiContent)(spec, sectionNames, baseUrlInput || undefined);
192
205
  // Find position to insert - after the swagger line
@@ -296,10 +309,12 @@ var debug = false
296
309
  // Create coverage status bar item
297
310
  const coverageStatusBarItem = (0, environmentProvider_1.createCoverageStatusBarItem)();
298
311
  // Register command to show coverage panel
299
- const showCoverageCommand = vscode.commands.registerCommand('norn.showCoverage', async () => {
312
+ const showCoverageCommand = vscode.commands.registerCommand('norn.showCoverage', async (nornapiFilePath) => {
300
313
  try {
301
- const coverage = await (0, coverageCalculator_1.getCoverage)();
302
- coveragePanel_1.CoveragePanel.show(coverage);
314
+ const coverage = nornapiFilePath
315
+ ? await (0, coverageCalculator_1.getCoverageForNornapiFile)(nornapiFilePath)
316
+ : await (0, coverageCalculator_1.getCoverage)();
317
+ coveragePanel_1.CoveragePanel.show(coverage, nornapiFilePath);
303
318
  }
304
319
  catch (error) {
305
320
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -307,10 +322,12 @@ var debug = false
307
322
  }
308
323
  });
309
324
  // Register command to refresh coverage
310
- const refreshCoverageCommand = vscode.commands.registerCommand('norn.refreshCoverage', async () => {
325
+ const refreshCoverageCommand = vscode.commands.registerCommand('norn.refreshCoverage', async (nornapiFilePath) => {
311
326
  try {
312
327
  (0, swaggerParser_1.invalidateSwaggerCache)();
313
- const coverage = await (0, coverageCalculator_1.refreshCoverage)();
328
+ const coverage = nornapiFilePath
329
+ ? await (0, coverageCalculator_1.refreshCoverageForNornapiFile)(nornapiFilePath)
330
+ : await (0, coverageCalculator_1.refreshCoverage)();
314
331
  coveragePanel_1.CoveragePanel.updateContent(coverage);
315
332
  vscode.window.showInformationMessage(`Coverage refreshed: ${coverage.percentage}%`);
316
333
  }
@@ -319,6 +336,39 @@ var debug = false
319
336
  vscode.window.showErrorMessage(`Failed to refresh coverage: ${errorMessage}`);
320
337
  }
321
338
  });
339
+ const refreshSwaggerBodySchemasFromUrls = async (urls) => {
340
+ for (const swaggerUrl of urls) {
341
+ try {
342
+ const spec = await (0, swaggerParser_1.parseSwaggerSpec)(swaggerUrl);
343
+ const requestSchemas = await (0, swaggerParser_1.extractRequestBodySchemas)(swaggerUrl);
344
+ (0, swaggerBodyIntellisenseCache_1.saveRequestBodySchemasForUrl)(swaggerUrl, spec.baseUrl, requestSchemas);
345
+ }
346
+ catch {
347
+ // Best effort cache refresh for IntelliSense
348
+ }
349
+ }
350
+ };
351
+ const warmSwaggerBodySchemaCacheFromWorkspace = async () => {
352
+ try {
353
+ const nornapiFiles = await vscode.workspace.findFiles('**/*.nornapi', '**/node_modules/**');
354
+ const urls = new Set();
355
+ for (const file of nornapiFiles) {
356
+ try {
357
+ const content = fsSync.readFileSync(file.fsPath, 'utf-8');
358
+ for (const url of extractSwaggerUrlsFromNornapi(content)) {
359
+ urls.add(url);
360
+ }
361
+ }
362
+ catch {
363
+ // Ignore individual file read errors
364
+ }
365
+ }
366
+ await refreshSwaggerBodySchemasFromUrls(Array.from(urls));
367
+ }
368
+ catch {
369
+ // Ignore workspace scan failures
370
+ }
371
+ };
322
372
  // Listen for coverage updates to update status bar and CodeLens
323
373
  const coverageUpdateListener = (0, coverageCalculator_1.onCoverageUpdate)((coverage) => {
324
374
  (0, environmentProvider_1.updateCoverageStatusBar)(coverage.hasSwagger, coverage.percentage, coverage.total, coverage.covered);
@@ -355,6 +405,12 @@ var debug = false
355
405
  if (document.languageId === 'norn' || document.languageId === 'nornapi') {
356
406
  scheduleCoverageUpdate();
357
407
  }
408
+ if (document.languageId === 'nornapi') {
409
+ const swaggerUrls = extractSwaggerUrlsFromNornapi(document.getText());
410
+ if (swaggerUrls.length > 0) {
411
+ void refreshSwaggerBodySchemasFromUrls(swaggerUrls);
412
+ }
413
+ }
358
414
  });
359
415
  // Initial coverage calculation (delayed to not slow down activation)
360
416
  setTimeout(async () => {
@@ -365,6 +421,10 @@ var debug = false
365
421
  // Ignore errors in initial coverage calculation
366
422
  }
367
423
  }, 2000);
424
+ // Warm Swagger request-body cache in background for IntelliSense.
425
+ setTimeout(() => {
426
+ void warmSwaggerBodySchemaCacheFromWorkspace();
427
+ }, 2500);
368
428
  // Create status bar item for environment
369
429
  const statusBarItem = (0, environmentProvider_1.createStatusBarItem)();
370
430
  // Register the CodeLens provider for .norn and .nornapi files
@@ -456,7 +516,7 @@ var debug = false
456
516
  }
457
517
  // Get variables and run imports
458
518
  const fileLevelVariables = (0, parser_1.extractFileLevelVariables)(fullText);
459
- const envVars = (0, environmentProvider_1.getEnvironmentVariables)();
519
+ const envVars = (0, environmentProvider_1.getEnvironmentVariables)(activeEditor.document.uri.fsPath);
460
520
  const allVariables = { ...envVars, ...fileLevelVariables };
461
521
  const importResult = await (0, parser_1.resolveImports)(fullText, workingDir, async (filePath) => {
462
522
  const content = await fs.readFile(filePath, 'utf-8');
@@ -528,8 +588,17 @@ var debug = false
528
588
  const onDidOpenDocument = vscode.workspace.onDidOpenTextDocument((document) => {
529
589
  if (document.languageId === 'norn' || document.languageId === 'nornenv' || document.languageId === 'nornapi') {
530
590
  diagnosticProvider.updateDiagnostics(document);
591
+ (0, environmentProvider_1.refreshEnvironmentStatusBar)();
592
+ (0, environmentProvider_1.refreshCoverageStatusBarContext)();
593
+ vscode.commands.executeCommand('norn.refreshCodeLenses');
531
594
  }
532
595
  });
596
+ // Refresh environment context when switching active editors
597
+ const onDidChangeActiveEditor = vscode.window.onDidChangeActiveTextEditor(() => {
598
+ (0, environmentProvider_1.refreshEnvironmentStatusBar)();
599
+ (0, environmentProvider_1.refreshCoverageStatusBarContext)();
600
+ vscode.commands.executeCommand('norn.refreshCodeLenses');
601
+ });
533
602
  // Clear diagnostics when document closes
534
603
  const onDidCloseDocument = vscode.workspace.onDidCloseTextDocument((document) => {
535
604
  diagnosticProvider.clearDiagnostics(document);
@@ -540,7 +609,7 @@ var debug = false
540
609
  diagnosticProvider.updateDiagnostics(document);
541
610
  }
542
611
  });
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 });
612
+ 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, onDidChangeActiveEditor, onDidCloseDocument, nornapiFileWatcher, documentSaveWatcher, { dispose: environmentProvider_1.disposeStatusBar });
544
613
  }
545
614
  function formatSequenceCaseLabel(caseParams, index) {
546
615
  const entries = Object.entries(caseParams);
@@ -623,7 +692,7 @@ async function processSequence(extensionUri, lineFromCodeLens) {
623
692
  }
624
693
  // Merge environment variables with file-level variables (outside sequences)
625
694
  // Variables inside sequences are local to that sequence
626
- const envVariables = (0, environmentProvider_1.getEnvironmentVariables)();
695
+ const envVariables = (0, environmentProvider_1.getEnvironmentVariables)(editor.document.uri.fsPath);
627
696
  const fileVariables = (0, parser_1.extractFileLevelVariables)(fullText);
628
697
  const allVariables = { ...envVariables, ...fileVariables };
629
698
  // Apply default parameter values for sequences with all-optional parameters
@@ -831,7 +900,7 @@ async function processEditorsInput(extensionUri, lineFromCodeLens) {
831
900
  const workingDir = path.dirname(editor.document.uri.fsPath);
832
901
  try {
833
902
  // Extract variables: environment + file (file takes precedence)
834
- const envVariables = (0, environmentProvider_1.getEnvironmentVariables)();
903
+ const envVariables = (0, environmentProvider_1.getEnvironmentVariables)(editor.document.uri.fsPath);
835
904
  const fileVariables = (0, parser_1.extractVariables)(fullText);
836
905
  const variables = { ...envVariables, ...fileVariables };
837
906
  // Resolve imports to get API definitions (endpoints and header groups)
@@ -1017,6 +1086,26 @@ function debounce(fn, delay) {
1017
1086
  }, delay);
1018
1087
  });
1019
1088
  }
1089
+ function extractSwaggerUrlsFromNornapi(content) {
1090
+ const urls = new Set();
1091
+ const lines = content.split('\n');
1092
+ for (const line of lines) {
1093
+ const trimmed = line.trim();
1094
+ if (!trimmed || trimmed.startsWith('#')) {
1095
+ continue;
1096
+ }
1097
+ const quotedMatch = trimmed.match(/^swagger\s+["']([^"']+)["']\s*$/i);
1098
+ if (quotedMatch) {
1099
+ urls.add(quotedMatch[1]);
1100
+ continue;
1101
+ }
1102
+ const unquotedMatch = trimmed.match(/^swagger\s+(https?:\/\/\S+)\s*$/i);
1103
+ if (unquotedMatch) {
1104
+ urls.add(unquotedMatch[1]);
1105
+ }
1106
+ }
1107
+ return Array.from(urls);
1108
+ }
1020
1109
  function deactivate() {
1021
1110
  // Clear caches on deactivation
1022
1111
  (0, swaggerParser_1.clearSwaggerCache)();