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.
- package/.norn-cache/swagger-body-intellisense.json +1 -1
- package/CHANGELOG.md +16 -0
- package/out/chatParticipant.js +722 -0
- package/out/cli.js +99 -36
- package/out/codeLensProvider.js +14 -20
- package/out/completionProvider.js +543 -25
- package/out/coverageCalculator.js +250 -169
- package/out/coveragePanel.js +7 -4
- package/out/diagnosticProvider.js +135 -2
- package/out/environmentProvider.js +96 -27
- package/out/extension.js +98 -9
- package/out/nornPrompt.js +580 -0
- package/out/swaggerBodyIntellisenseCache.js +147 -0
- package/out/swaggerParser.js +154 -74
- package/out/test/coverageCalculator.test.js +100 -0
- package/out/testProvider.js +1 -1
- package/package.json +1 -1
|
@@ -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, {
|
|
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 =
|
|
79
|
-
|
|
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
|
-
|
|
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(
|
|
156
|
-
const filePath =
|
|
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(
|
|
185
|
-
const config = loadEnvironmentConfig(
|
|
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(
|
|
204
|
-
const config = loadEnvironmentConfig(
|
|
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 =
|
|
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
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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 =
|
|
302
|
-
|
|
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 =
|
|
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)();
|