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
|
@@ -41,6 +41,7 @@ const parser_1 = require("./parser");
|
|
|
41
41
|
const sequenceRunner_1 = require("./sequenceRunner");
|
|
42
42
|
const environmentProvider_1 = require("./environmentProvider");
|
|
43
43
|
const nornapiParser_1 = require("./nornapiParser");
|
|
44
|
+
const swaggerBodyIntellisenseCache_1 = require("./swaggerBodyIntellisenseCache");
|
|
44
45
|
class HttpCompletionProvider {
|
|
45
46
|
httpMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
|
|
46
47
|
keywords = ['var', 'test', 'test sequence', 'sequence', 'end sequence', 'if', 'end if', 'wait', 'run bash', 'run powershell', 'run js', 'run readJson', 'run', 'print', 'assert', 'import', 'return', 'retry', 'backoff'];
|
|
@@ -140,15 +141,26 @@ class HttpCompletionProvider {
|
|
|
140
141
|
if (this.isTypingPropertyAssignment(linePrefix)) {
|
|
141
142
|
return this.getPropertyAssignmentCompletions(document, linePrefix);
|
|
142
143
|
}
|
|
143
|
-
//
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
144
|
+
// Swagger-based request body IntelliSense for endpoint POST/PUT/PATCH calls.
|
|
145
|
+
const requestBodyContext = this.getRequestBodyCompletionContext(document, position);
|
|
146
|
+
if (requestBodyContext) {
|
|
147
|
+
if (this.isTypingJsonBodyStart(linePrefix)) {
|
|
148
|
+
return this.getRequestBodyTemplateCompletions(requestBodyContext, position, lineText);
|
|
149
|
+
}
|
|
150
|
+
const inlineBodyCompletions = this.getInlineRequestBodyKeyCompletions(requestBodyContext, document, position, linePrefix);
|
|
151
|
+
if (inlineBodyCompletions.length > 0) {
|
|
152
|
+
return inlineBodyCompletions;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const isAfterApiRequest = this.isAfterApiRequest(document, position);
|
|
156
|
+
// If a JSON body is being started right below an API request, don't show IntelliSense.
|
|
157
|
+
// We don't know body shape, and header/group suggestions are noisy in this context.
|
|
158
|
+
if (isAfterApiRequest && this.isTypingJsonBodyStart(linePrefix)) {
|
|
159
|
+
return [];
|
|
148
160
|
}
|
|
149
161
|
// Check if we're after an API request (GET EndpointName, etc.)
|
|
150
162
|
// Provide header groups and inline headers
|
|
151
|
-
if (
|
|
163
|
+
if (isAfterApiRequest) {
|
|
152
164
|
// If typing a header name (no colon yet), provide both header groups and inline headers
|
|
153
165
|
if (!linePrefix.includes(':')) {
|
|
154
166
|
const headerGroupCompletions = this.getStandaloneHeaderGroupCompletions(document, linePrefix);
|
|
@@ -158,6 +170,12 @@ class HttpCompletionProvider {
|
|
|
158
170
|
// If line has a colon but not Content-Type (which is handled earlier), return empty
|
|
159
171
|
return [];
|
|
160
172
|
}
|
|
173
|
+
// Check if user might be typing a JSON variable name for property assignment
|
|
174
|
+
const jsonVarCompletions = this.getJsonVariableCompletions(document, linePrefix);
|
|
175
|
+
// If we have JSON variable completions, show those (for property assignment)
|
|
176
|
+
if (jsonVarCompletions.length > 0) {
|
|
177
|
+
return jsonVarCompletions;
|
|
178
|
+
}
|
|
161
179
|
// Show HTTP methods/keywords at line start (or while typing them),
|
|
162
180
|
// but not after a METHOD ... context where endpoint/header logic applies.
|
|
163
181
|
if ((position.character === 0 || linePrefix.trim() === '' || this.couldBeMethodOrKeyword(trimmedPrefix)) &&
|
|
@@ -230,6 +248,13 @@ class HttpCompletionProvider {
|
|
|
230
248
|
}
|
|
231
249
|
return inSingleQuote || inDoubleQuote;
|
|
232
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Detect the start of a JSON body line (e.g., "{" or "[") under an API request.
|
|
253
|
+
*/
|
|
254
|
+
isTypingJsonBodyStart(linePrefix) {
|
|
255
|
+
const trimmed = linePrefix.trimStart();
|
|
256
|
+
return trimmed.startsWith('{') || trimmed.startsWith('[');
|
|
257
|
+
}
|
|
233
258
|
/**
|
|
234
259
|
* Check if user is in a context where bare variable names should be suggested.
|
|
235
260
|
* This includes:
|
|
@@ -311,7 +336,7 @@ class HttpCompletionProvider {
|
|
|
311
336
|
getBareVariableCompletions(document, position, linePrefix) {
|
|
312
337
|
const fullText = document.getText();
|
|
313
338
|
const fileVariables = (0, parser_1.extractVariables)(fullText);
|
|
314
|
-
const envVariables = (0, environmentProvider_1.getEnvironmentVariables)();
|
|
339
|
+
const envVariables = (0, environmentProvider_1.getEnvironmentVariables)(document.uri.fsPath);
|
|
315
340
|
// Get sequence-local variables if we're inside a sequence
|
|
316
341
|
const sequences = (0, sequenceRunner_1.extractSequences)(fullText);
|
|
317
342
|
const cursorLine = position.line;
|
|
@@ -397,6 +422,11 @@ class HttpCompletionProvider {
|
|
|
397
422
|
// Only trigger when there's content before the {
|
|
398
423
|
isTypingVariable(linePrefix) {
|
|
399
424
|
const trimmed = linePrefix.trim();
|
|
425
|
+
// JSON body/object start should not trigger variable completions.
|
|
426
|
+
// Body IntelliSense handles this context separately.
|
|
427
|
+
if (trimmed === '{' || trimmed === '[') {
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
400
430
|
// Must have something before the brace (not just starting with {)
|
|
401
431
|
// e.g., "GET {{" or "Authorization: Bearer {{"
|
|
402
432
|
// Check for {{ with content before it
|
|
@@ -741,12 +771,23 @@ class HttpCompletionProvider {
|
|
|
741
771
|
* Load API definitions (header groups and endpoints) from imported .nornapi files.
|
|
742
772
|
*/
|
|
743
773
|
getApiDefinitionsFromImports(document) {
|
|
774
|
+
const apiMetadata = this.getApiImportMetadata(document);
|
|
775
|
+
return {
|
|
776
|
+
headerGroups: apiMetadata.headerGroups,
|
|
777
|
+
endpoints: apiMetadata.endpoints
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Load imported .nornapi files with endpoint/header data and swagger URL metadata.
|
|
782
|
+
*/
|
|
783
|
+
getApiImportMetadata(document) {
|
|
744
784
|
const fullText = document.getText();
|
|
745
785
|
const imports = (0, parser_1.extractImports)(fullText);
|
|
746
786
|
const headerGroups = [];
|
|
747
787
|
const endpoints = [];
|
|
788
|
+
const apiFiles = [];
|
|
748
789
|
if (imports.length === 0) {
|
|
749
|
-
return { headerGroups, endpoints };
|
|
790
|
+
return { headerGroups, endpoints, apiFiles };
|
|
750
791
|
}
|
|
751
792
|
const documentDir = path.dirname(document.uri.fsPath);
|
|
752
793
|
for (const imp of imports) {
|
|
@@ -760,12 +801,486 @@ class HttpCompletionProvider {
|
|
|
760
801
|
const apiDef = (0, nornapiParser_1.parseNornApiFile)(importedContent);
|
|
761
802
|
headerGroups.push(...apiDef.headerGroups);
|
|
762
803
|
endpoints.push(...apiDef.endpoints);
|
|
804
|
+
apiFiles.push({
|
|
805
|
+
sourcePath: importPath,
|
|
806
|
+
headerGroups: apiDef.headerGroups,
|
|
807
|
+
endpoints: apiDef.endpoints,
|
|
808
|
+
swaggerUrls: this.extractSwaggerUrlsFromNornapi(importedContent)
|
|
809
|
+
});
|
|
763
810
|
}
|
|
764
811
|
catch {
|
|
765
812
|
// Ignore import errors
|
|
766
813
|
}
|
|
767
814
|
}
|
|
768
|
-
return { headerGroups, endpoints };
|
|
815
|
+
return { headerGroups, endpoints, apiFiles };
|
|
816
|
+
}
|
|
817
|
+
extractSwaggerUrlsFromNornapi(content) {
|
|
818
|
+
const urls = new Set();
|
|
819
|
+
const lines = content.split('\n');
|
|
820
|
+
for (const line of lines) {
|
|
821
|
+
const trimmed = line.trim();
|
|
822
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
const quotedMatch = trimmed.match(/^swagger\s+["']([^"']+)["']\s*$/i);
|
|
826
|
+
if (quotedMatch) {
|
|
827
|
+
urls.add(quotedMatch[1]);
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
const unquotedMatch = trimmed.match(/^swagger\s+(https?:\/\/\S+)\s*$/i);
|
|
831
|
+
if (unquotedMatch) {
|
|
832
|
+
urls.add(unquotedMatch[1]);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return Array.from(urls);
|
|
836
|
+
}
|
|
837
|
+
getRequestBodyCompletionContext(document, position) {
|
|
838
|
+
const apiMetadata = this.getApiImportMetadata(document);
|
|
839
|
+
if (apiMetadata.endpoints.length === 0 || apiMetadata.apiFiles.length === 0) {
|
|
840
|
+
return undefined;
|
|
841
|
+
}
|
|
842
|
+
const endpointSchemaMap = this.getEndpointRequestBodySchemaMap(apiMetadata);
|
|
843
|
+
if (endpointSchemaMap.size === 0) {
|
|
844
|
+
return undefined;
|
|
845
|
+
}
|
|
846
|
+
const headerGroupNames = new Set(apiMetadata.headerGroups.map(group => group.name));
|
|
847
|
+
for (let lineNum = position.line; lineNum >= 0 && lineNum >= position.line - 80; lineNum--) {
|
|
848
|
+
const trimmed = document.lineAt(lineNum).text.trim();
|
|
849
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
const requestMatch = trimmed.match(/^(?:var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*)?(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+([a-zA-Z_][a-zA-Z0-9_]*)(?:\([^)]*\))?(?:\s+.*)?$/i);
|
|
853
|
+
if (requestMatch) {
|
|
854
|
+
const method = requestMatch[1].toUpperCase();
|
|
855
|
+
const endpointName = requestMatch[2];
|
|
856
|
+
if (!this.isRequestBodyMethod(method)) {
|
|
857
|
+
return undefined;
|
|
858
|
+
}
|
|
859
|
+
const mapped = endpointSchemaMap.get(endpointName);
|
|
860
|
+
if (!mapped) {
|
|
861
|
+
return undefined;
|
|
862
|
+
}
|
|
863
|
+
if (mapped.endpoint.method.toUpperCase() !== method) {
|
|
864
|
+
return undefined;
|
|
865
|
+
}
|
|
866
|
+
const bodyStartLine = this.findRequestBodyStartLine(document, lineNum, position.line, headerGroupNames);
|
|
867
|
+
if (bodyStartLine === undefined || position.line < bodyStartLine) {
|
|
868
|
+
return undefined;
|
|
869
|
+
}
|
|
870
|
+
return {
|
|
871
|
+
method,
|
|
872
|
+
endpointName,
|
|
873
|
+
endpoint: mapped.endpoint,
|
|
874
|
+
schema: mapped.schema,
|
|
875
|
+
bodyStartLine
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
if (lineNum !== position.line && this.isBoundaryCommandLine(trimmed)) {
|
|
879
|
+
return undefined;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return undefined;
|
|
883
|
+
}
|
|
884
|
+
getEndpointRequestBodySchemaMap(apiMetadata) {
|
|
885
|
+
const endpointSchemaMap = new Map();
|
|
886
|
+
for (const apiFile of apiMetadata.apiFiles) {
|
|
887
|
+
if (apiFile.swaggerUrls.length === 0 || apiFile.endpoints.length === 0) {
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
const cacheEntries = apiFile.swaggerUrls
|
|
891
|
+
.map(url => (0, swaggerBodyIntellisenseCache_1.getCachedRequestBodySchemasForUrl)(url))
|
|
892
|
+
.filter((entry) => !!entry);
|
|
893
|
+
if (cacheEntries.length === 0) {
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
for (const endpoint of apiFile.endpoints) {
|
|
897
|
+
if (!this.isRequestBodyMethod(endpoint.method) || endpointSchemaMap.has(endpoint.name)) {
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
const method = endpoint.method.toUpperCase();
|
|
901
|
+
for (const cacheEntry of cacheEntries) {
|
|
902
|
+
const normalizedPath = this.normalizeEndpointPathForSwagger(endpoint.path, cacheEntry.baseUrl);
|
|
903
|
+
if (!normalizedPath) {
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
const matchedSchema = cacheEntry.schemas.find(schema => schema.method.toUpperCase() === method &&
|
|
907
|
+
schema.path === normalizedPath);
|
|
908
|
+
if (!matchedSchema) {
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
endpointSchemaMap.set(endpoint.name, {
|
|
912
|
+
endpoint,
|
|
913
|
+
schema: matchedSchema
|
|
914
|
+
});
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return endpointSchemaMap;
|
|
920
|
+
}
|
|
921
|
+
normalizeEndpointPathForSwagger(endpointPath, baseUrl) {
|
|
922
|
+
const rawPath = endpointPath.trim();
|
|
923
|
+
if (!rawPath) {
|
|
924
|
+
return undefined;
|
|
925
|
+
}
|
|
926
|
+
const getBasePath = () => {
|
|
927
|
+
if (!baseUrl) {
|
|
928
|
+
return '';
|
|
929
|
+
}
|
|
930
|
+
try {
|
|
931
|
+
const parsed = new URL(baseUrl);
|
|
932
|
+
return parsed.pathname.replace(/\/$/, '');
|
|
933
|
+
}
|
|
934
|
+
catch {
|
|
935
|
+
return '';
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
const basePath = getBasePath();
|
|
939
|
+
const normalizeAndStripBasePath = (candidatePath) => {
|
|
940
|
+
const withoutQuery = candidatePath.split(/[?#]/)[0];
|
|
941
|
+
let normalized = withoutQuery.startsWith('/') ? withoutQuery : `/${withoutQuery}`;
|
|
942
|
+
if (basePath) {
|
|
943
|
+
if (normalized === basePath) {
|
|
944
|
+
return '/';
|
|
945
|
+
}
|
|
946
|
+
if (normalized.startsWith(`${basePath}/`)) {
|
|
947
|
+
normalized = normalized.slice(basePath.length);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return normalized || '/';
|
|
951
|
+
};
|
|
952
|
+
// Handle variable-prefix paths: {{baseUrl}}/users, {{apiRoot}}/v2/users, etc.
|
|
953
|
+
const variablePrefixMatch = rawPath.match(/^\{\{[^}]+\}\}(.*)$/);
|
|
954
|
+
if (variablePrefixMatch) {
|
|
955
|
+
const suffix = variablePrefixMatch[1] || '';
|
|
956
|
+
return normalizeAndStripBasePath(suffix || '/');
|
|
957
|
+
}
|
|
958
|
+
// Most imported endpoints are generated as full URLs, so strip baseUrl first when possible.
|
|
959
|
+
if (baseUrl && rawPath.startsWith(baseUrl)) {
|
|
960
|
+
const stripped = rawPath.slice(baseUrl.length);
|
|
961
|
+
return normalizeAndStripBasePath(stripped || '/');
|
|
962
|
+
}
|
|
963
|
+
if (/^https?:\/\//i.test(rawPath)) {
|
|
964
|
+
try {
|
|
965
|
+
const endpointUrl = new URL(rawPath);
|
|
966
|
+
if (baseUrl) {
|
|
967
|
+
try {
|
|
968
|
+
const parsedBaseUrl = new URL(baseUrl, endpointUrl.origin);
|
|
969
|
+
const basePath = parsedBaseUrl.pathname.replace(/\/$/, '');
|
|
970
|
+
// Compare by host to tolerate scheme differences (http vs https).
|
|
971
|
+
if (endpointUrl.hostname === parsedBaseUrl.hostname) {
|
|
972
|
+
if (basePath && endpointUrl.pathname.startsWith(`${basePath}/`)) {
|
|
973
|
+
return endpointUrl.pathname.slice(basePath.length);
|
|
974
|
+
}
|
|
975
|
+
if (basePath && endpointUrl.pathname === basePath) {
|
|
976
|
+
return '/';
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
catch {
|
|
981
|
+
// Ignore malformed base URL values (e.g., templated server URLs)
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
return normalizeAndStripBasePath(endpointUrl.pathname || '/');
|
|
985
|
+
}
|
|
986
|
+
catch {
|
|
987
|
+
return undefined;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return normalizeAndStripBasePath(rawPath);
|
|
991
|
+
}
|
|
992
|
+
findRequestBodyStartLine(document, requestLine, currentLine, headerGroupNames) {
|
|
993
|
+
for (let lineNum = requestLine + 1; lineNum <= currentLine; lineNum++) {
|
|
994
|
+
const trimmed = document.lineAt(lineNum).text.trim();
|
|
995
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
if (headerGroupNames.has(trimmed)) {
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
if (this.isInlineHeaderLine(trimmed)) {
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
return lineNum;
|
|
1005
|
+
}
|
|
1006
|
+
return undefined;
|
|
1007
|
+
}
|
|
1008
|
+
isInlineHeaderLine(trimmedLine) {
|
|
1009
|
+
return /^[A-Za-z][A-Za-z0-9-]*:\s*.+$/.test(trimmedLine);
|
|
1010
|
+
}
|
|
1011
|
+
isBoundaryCommandLine(trimmedLine) {
|
|
1012
|
+
return /^(###|\[|(?:test\s+)?sequence\b|end\s+sequence\b|end\s+if\b|endif\b|var\s+|run\s+|print\s+|assert\s+|if\s+|wait\s+|import\s+|return\s+|swagger\b|(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+)/i.test(trimmedLine);
|
|
1013
|
+
}
|
|
1014
|
+
isRequestBodyMethod(method) {
|
|
1015
|
+
return method === 'POST' || method === 'PUT' || method === 'PATCH';
|
|
1016
|
+
}
|
|
1017
|
+
getRequestBodyTemplateCompletions(context, position, lineText) {
|
|
1018
|
+
const templateValue = this.buildTemplateValueFromSchema(context.schema.schema);
|
|
1019
|
+
const rawTemplate = JSON.stringify(templateValue, null, 4);
|
|
1020
|
+
if (!rawTemplate) {
|
|
1021
|
+
return [];
|
|
1022
|
+
}
|
|
1023
|
+
const indentation = lineText.match(/^\s*/)[0];
|
|
1024
|
+
const indentedTemplate = rawTemplate
|
|
1025
|
+
.split('\n')
|
|
1026
|
+
.map(line => indentation + line)
|
|
1027
|
+
.join('\n');
|
|
1028
|
+
const replaceRange = new vscode.Range(new vscode.Position(position.line, 0), new vscode.Position(position.line, lineText.length));
|
|
1029
|
+
const item = new vscode.CompletionItem('Insert request body template', vscode.CompletionItemKind.Snippet);
|
|
1030
|
+
item.insertText = indentedTemplate;
|
|
1031
|
+
item.range = replaceRange;
|
|
1032
|
+
const triggerChar = lineText.trimStart().startsWith('[') ? '[' : '{';
|
|
1033
|
+
item.filterText = triggerChar;
|
|
1034
|
+
item.preselect = true;
|
|
1035
|
+
item.detail = `${context.method} ${context.endpointName}`;
|
|
1036
|
+
item.documentation = new vscode.MarkdownString(`Insert a request body skeleton for \`${context.endpointName}\` from cached Swagger schema.\n\n` +
|
|
1037
|
+
'Template includes required fields only.');
|
|
1038
|
+
item.sortText = '0_request_body_template';
|
|
1039
|
+
return [item];
|
|
1040
|
+
}
|
|
1041
|
+
buildTemplateValueFromSchema(schema) {
|
|
1042
|
+
const normalizedSchema = this.normalizeSchemaForCompletions(schema);
|
|
1043
|
+
const value = this.buildDefaultSchemaValue(normalizedSchema, true);
|
|
1044
|
+
return value === undefined ? {} : value;
|
|
1045
|
+
}
|
|
1046
|
+
buildDefaultSchemaValue(schema, requiredOnly) {
|
|
1047
|
+
const normalizedSchema = this.normalizeSchemaForCompletions(schema);
|
|
1048
|
+
if (!normalizedSchema || typeof normalizedSchema !== 'object') {
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
if (normalizedSchema.default !== undefined) {
|
|
1052
|
+
return normalizedSchema.default;
|
|
1053
|
+
}
|
|
1054
|
+
if (Array.isArray(normalizedSchema.enum) && normalizedSchema.enum.length > 0) {
|
|
1055
|
+
return normalizedSchema.enum[0];
|
|
1056
|
+
}
|
|
1057
|
+
if (normalizedSchema.type === 'object' || normalizedSchema.properties) {
|
|
1058
|
+
const properties = normalizedSchema.properties;
|
|
1059
|
+
if (!properties || Object.keys(properties).length === 0) {
|
|
1060
|
+
return {};
|
|
1061
|
+
}
|
|
1062
|
+
const required = Array.isArray(normalizedSchema.required)
|
|
1063
|
+
? normalizedSchema.required.filter((name) => typeof name === 'string')
|
|
1064
|
+
: [];
|
|
1065
|
+
const keys = requiredOnly
|
|
1066
|
+
? (required.length > 0 ? required : Object.keys(properties))
|
|
1067
|
+
: Object.keys(properties);
|
|
1068
|
+
const result = {};
|
|
1069
|
+
for (const key of keys) {
|
|
1070
|
+
result[key] = this.buildDefaultSchemaValue(properties[key], requiredOnly);
|
|
1071
|
+
}
|
|
1072
|
+
return result;
|
|
1073
|
+
}
|
|
1074
|
+
if (normalizedSchema.type === 'array' || normalizedSchema.items) {
|
|
1075
|
+
return [];
|
|
1076
|
+
}
|
|
1077
|
+
switch (normalizedSchema.type) {
|
|
1078
|
+
case 'number':
|
|
1079
|
+
case 'integer':
|
|
1080
|
+
return 0;
|
|
1081
|
+
case 'boolean':
|
|
1082
|
+
return false;
|
|
1083
|
+
case 'string':
|
|
1084
|
+
return '';
|
|
1085
|
+
case 'null':
|
|
1086
|
+
return null;
|
|
1087
|
+
default:
|
|
1088
|
+
return null;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
getInlineRequestBodyKeyCompletions(context, document, position, linePrefix) {
|
|
1092
|
+
if (!this.isTypingJsonObjectKey(linePrefix)) {
|
|
1093
|
+
return [];
|
|
1094
|
+
}
|
|
1095
|
+
const bodyLinesBeforeCursor = [];
|
|
1096
|
+
for (let lineNum = context.bodyStartLine; lineNum < position.line; lineNum++) {
|
|
1097
|
+
bodyLinesBeforeCursor.push(document.lineAt(lineNum).text);
|
|
1098
|
+
}
|
|
1099
|
+
const jsonContext = this.parseJsonObjectContext(bodyLinesBeforeCursor);
|
|
1100
|
+
const schemaNode = this.getSchemaNodeForPath(context.schema.schema, jsonContext.path);
|
|
1101
|
+
const normalizedSchema = this.normalizeSchemaForCompletions(schemaNode);
|
|
1102
|
+
const properties = normalizedSchema?.properties;
|
|
1103
|
+
if (!properties || Object.keys(properties).length === 0) {
|
|
1104
|
+
return [];
|
|
1105
|
+
}
|
|
1106
|
+
const required = new Set(Array.isArray(normalizedSchema.required)
|
|
1107
|
+
? normalizedSchema.required.filter((name) => typeof name === 'string')
|
|
1108
|
+
: []);
|
|
1109
|
+
const replaceTokenMatch = linePrefix.match(/"?[a-zA-Z0-9_-]*$/);
|
|
1110
|
+
const replaceToken = replaceTokenMatch ? replaceTokenMatch[0] : '';
|
|
1111
|
+
const partialMatch = linePrefix.match(/"?([a-zA-Z_][a-zA-Z0-9_-]*)?$/);
|
|
1112
|
+
const partial = partialMatch?.[1]?.toLowerCase() || '';
|
|
1113
|
+
const replaceStart = Math.max(0, position.character - replaceToken.length);
|
|
1114
|
+
const replaceRange = new vscode.Range(new vscode.Position(position.line, replaceStart), position);
|
|
1115
|
+
const items = [];
|
|
1116
|
+
for (const [propertyName, propertySchema] of Object.entries(properties)) {
|
|
1117
|
+
if (jsonContext.existingKeys.has(propertyName)) {
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
if (partial && !propertyName.toLowerCase().startsWith(partial)) {
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
const item = new vscode.CompletionItem(propertyName, vscode.CompletionItemKind.Property);
|
|
1124
|
+
item.range = replaceRange;
|
|
1125
|
+
item.insertText = `"${propertyName}": `;
|
|
1126
|
+
item.detail = required.has(propertyName)
|
|
1127
|
+
? `${this.getSchemaTypeDescription(propertySchema)} (required)`
|
|
1128
|
+
: this.getSchemaTypeDescription(propertySchema);
|
|
1129
|
+
item.documentation = new vscode.MarkdownString(`Schema property for \`${context.endpointName}\` request body.`);
|
|
1130
|
+
item.sortText = required.has(propertyName)
|
|
1131
|
+
? `0_${propertyName}`
|
|
1132
|
+
: `1_${propertyName}`;
|
|
1133
|
+
items.push(item);
|
|
1134
|
+
}
|
|
1135
|
+
return items;
|
|
1136
|
+
}
|
|
1137
|
+
isTypingJsonObjectKey(linePrefix) {
|
|
1138
|
+
const trimmed = linePrefix.trim();
|
|
1139
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('}') || trimmed.startsWith(']')) {
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
if (trimmed.includes(':') || trimmed.includes(',')) {
|
|
1143
|
+
return false;
|
|
1144
|
+
}
|
|
1145
|
+
if (trimmed === '' || trimmed === '"') {
|
|
1146
|
+
return true;
|
|
1147
|
+
}
|
|
1148
|
+
return /^"?[a-zA-Z_][a-zA-Z0-9_-]*$/.test(trimmed);
|
|
1149
|
+
}
|
|
1150
|
+
parseJsonObjectContext(lines) {
|
|
1151
|
+
const pathStack = [];
|
|
1152
|
+
const keysByPath = new Map();
|
|
1153
|
+
const getPathKey = () => pathStack.join('.');
|
|
1154
|
+
const ensureKeySet = (pathKey) => {
|
|
1155
|
+
if (!keysByPath.has(pathKey)) {
|
|
1156
|
+
keysByPath.set(pathKey, new Set());
|
|
1157
|
+
}
|
|
1158
|
+
return keysByPath.get(pathKey);
|
|
1159
|
+
};
|
|
1160
|
+
for (const line of lines) {
|
|
1161
|
+
const trimmed = line.trim();
|
|
1162
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
const closingMatch = trimmed.match(/^[}\]]+/);
|
|
1166
|
+
if (closingMatch) {
|
|
1167
|
+
for (const _ of closingMatch[0]) {
|
|
1168
|
+
if (pathStack.length > 0) {
|
|
1169
|
+
pathStack.pop();
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
const currentPath = getPathKey();
|
|
1174
|
+
const addExistingKey = (key) => {
|
|
1175
|
+
ensureKeySet(currentPath).add(key);
|
|
1176
|
+
};
|
|
1177
|
+
const objectStartMatch = trimmed.match(/^"([^"]+)"\s*:\s*\{\s*,?\s*$/);
|
|
1178
|
+
if (objectStartMatch) {
|
|
1179
|
+
addExistingKey(objectStartMatch[1]);
|
|
1180
|
+
pathStack.push(objectStartMatch[1]);
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
const arrayStartMatch = trimmed.match(/^"([^"]+)"\s*:\s*\[\s*,?\s*$/);
|
|
1184
|
+
if (arrayStartMatch) {
|
|
1185
|
+
addExistingKey(arrayStartMatch[1]);
|
|
1186
|
+
pathStack.push(arrayStartMatch[1]);
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
const propertyMatch = trimmed.match(/^"([^"]+)"\s*:/);
|
|
1190
|
+
if (propertyMatch) {
|
|
1191
|
+
addExistingKey(propertyMatch[1]);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
const finalPath = getPathKey();
|
|
1195
|
+
return {
|
|
1196
|
+
path: [...pathStack],
|
|
1197
|
+
existingKeys: new Set(keysByPath.get(finalPath) || [])
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
getSchemaNodeForPath(schema, pathSegments) {
|
|
1201
|
+
let currentSchema = this.normalizeSchemaForCompletions(schema);
|
|
1202
|
+
for (const segment of pathSegments) {
|
|
1203
|
+
currentSchema = this.normalizeSchemaForCompletions(currentSchema);
|
|
1204
|
+
if (!currentSchema || typeof currentSchema !== 'object') {
|
|
1205
|
+
return undefined;
|
|
1206
|
+
}
|
|
1207
|
+
const properties = currentSchema.properties;
|
|
1208
|
+
if (!properties || !(segment in properties)) {
|
|
1209
|
+
return undefined;
|
|
1210
|
+
}
|
|
1211
|
+
const propertySchema = this.normalizeSchemaForCompletions(properties[segment]);
|
|
1212
|
+
if (propertySchema?.type === 'array' && propertySchema.items) {
|
|
1213
|
+
currentSchema = this.normalizeSchemaForCompletions(propertySchema.items);
|
|
1214
|
+
}
|
|
1215
|
+
else {
|
|
1216
|
+
currentSchema = propertySchema;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
return this.normalizeSchemaForCompletions(currentSchema);
|
|
1220
|
+
}
|
|
1221
|
+
normalizeSchemaForCompletions(schema) {
|
|
1222
|
+
if (!schema || typeof schema !== 'object') {
|
|
1223
|
+
return schema;
|
|
1224
|
+
}
|
|
1225
|
+
if (Array.isArray(schema.allOf) && schema.allOf.length > 0) {
|
|
1226
|
+
const merged = { ...schema };
|
|
1227
|
+
delete merged.allOf;
|
|
1228
|
+
for (const part of schema.allOf) {
|
|
1229
|
+
const normalizedPart = this.normalizeSchemaForCompletions(part);
|
|
1230
|
+
if (!normalizedPart || typeof normalizedPart !== 'object') {
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
if (!merged.type && normalizedPart.type) {
|
|
1234
|
+
merged.type = normalizedPart.type;
|
|
1235
|
+
}
|
|
1236
|
+
if (normalizedPart.properties && typeof normalizedPart.properties === 'object') {
|
|
1237
|
+
merged.properties = {
|
|
1238
|
+
...(merged.properties || {}),
|
|
1239
|
+
...normalizedPart.properties
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
if (normalizedPart.items && !merged.items) {
|
|
1243
|
+
merged.items = normalizedPart.items;
|
|
1244
|
+
}
|
|
1245
|
+
if (Array.isArray(normalizedPart.required)) {
|
|
1246
|
+
const existing = Array.isArray(merged.required) ? merged.required : [];
|
|
1247
|
+
merged.required = Array.from(new Set([...existing, ...normalizedPart.required]));
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
return merged;
|
|
1251
|
+
}
|
|
1252
|
+
if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
|
|
1253
|
+
return this.normalizeSchemaForCompletions(schema.oneOf[0]);
|
|
1254
|
+
}
|
|
1255
|
+
if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
|
|
1256
|
+
return this.normalizeSchemaForCompletions(schema.anyOf[0]);
|
|
1257
|
+
}
|
|
1258
|
+
return schema;
|
|
1259
|
+
}
|
|
1260
|
+
getSchemaTypeDescription(schema) {
|
|
1261
|
+
const normalizedSchema = this.normalizeSchemaForCompletions(schema);
|
|
1262
|
+
if (!normalizedSchema || typeof normalizedSchema !== 'object') {
|
|
1263
|
+
return 'value';
|
|
1264
|
+
}
|
|
1265
|
+
if (Array.isArray(normalizedSchema.enum) && normalizedSchema.enum.length > 0) {
|
|
1266
|
+
const enumPreview = normalizedSchema.enum
|
|
1267
|
+
.slice(0, 3)
|
|
1268
|
+
.map((value) => JSON.stringify(value))
|
|
1269
|
+
.join(', ');
|
|
1270
|
+
return normalizedSchema.enum.length > 3
|
|
1271
|
+
? `enum (${enumPreview}, ...)`
|
|
1272
|
+
: `enum (${enumPreview})`;
|
|
1273
|
+
}
|
|
1274
|
+
if (typeof normalizedSchema.type === 'string') {
|
|
1275
|
+
return normalizedSchema.type;
|
|
1276
|
+
}
|
|
1277
|
+
if (normalizedSchema.properties) {
|
|
1278
|
+
return 'object';
|
|
1279
|
+
}
|
|
1280
|
+
if (normalizedSchema.items) {
|
|
1281
|
+
return 'array';
|
|
1282
|
+
}
|
|
1283
|
+
return 'value';
|
|
769
1284
|
}
|
|
770
1285
|
/**
|
|
771
1286
|
* Check if user is typing after an HTTP method (e.g., "GET " or "POST " or "var x = GET ")
|
|
@@ -956,16 +1471,15 @@ class HttpCompletionProvider {
|
|
|
956
1471
|
return items;
|
|
957
1472
|
}
|
|
958
1473
|
/**
|
|
959
|
-
* Check if we're on a line after an
|
|
960
|
-
*
|
|
961
|
-
*
|
|
1474
|
+
* Check if we're on a line after an HTTP request.
|
|
1475
|
+
* Supports:
|
|
1476
|
+
* - METHOD EndpointName / METHOD URL
|
|
1477
|
+
* - var x = METHOD EndpointName / URL
|
|
1478
|
+
* Allows inline headers and header groups on following lines.
|
|
962
1479
|
*/
|
|
963
1480
|
isAfterApiRequest(document, position) {
|
|
964
1481
|
const apiDefs = this.getApiDefinitionsFromImports(document);
|
|
965
|
-
if
|
|
966
|
-
return false;
|
|
967
|
-
}
|
|
968
|
-
// Look at previous lines to see if we're after an API request
|
|
1482
|
+
// Look at previous lines to see if we're after a request block start.
|
|
969
1483
|
for (let lineNum = position.line - 1; lineNum >= 0 && lineNum >= position.line - 10; lineNum--) {
|
|
970
1484
|
const prevLine = document.lineAt(lineNum).text.trim();
|
|
971
1485
|
// Skip empty lines
|
|
@@ -973,6 +1487,10 @@ class HttpCompletionProvider {
|
|
|
973
1487
|
// Empty line means we're past the request block
|
|
974
1488
|
return false;
|
|
975
1489
|
}
|
|
1490
|
+
// Skip comment lines
|
|
1491
|
+
if (prevLine.startsWith('#')) {
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
976
1494
|
// Skip header group names on their own line
|
|
977
1495
|
if (apiDefs.headerGroups.some(hg => hg.name === prevLine)) {
|
|
978
1496
|
continue;
|
|
@@ -981,13 +1499,13 @@ class HttpCompletionProvider {
|
|
|
981
1499
|
if (/^[A-Za-z][A-Za-z0-9-]*:\s*.+$/.test(prevLine)) {
|
|
982
1500
|
continue;
|
|
983
1501
|
}
|
|
984
|
-
//
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1502
|
+
// Request start: METHOD ...
|
|
1503
|
+
if (/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+.+$/i.test(prevLine)) {
|
|
1504
|
+
return true;
|
|
1505
|
+
}
|
|
1506
|
+
// Request start: var x = METHOD ...
|
|
1507
|
+
if (/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+.+$/i.test(prevLine)) {
|
|
1508
|
+
return true;
|
|
991
1509
|
}
|
|
992
1510
|
// If we hit another kind of line that's not recognized, stop
|
|
993
1511
|
break;
|
|
@@ -1062,7 +1580,7 @@ class HttpCompletionProvider {
|
|
|
1062
1580
|
getVariableCompletions(document, linePrefix, lineSuffix) {
|
|
1063
1581
|
const fullText = document.getText();
|
|
1064
1582
|
const fileVariables = (0, parser_1.extractVariables)(fullText);
|
|
1065
|
-
const envVariables = (0, environmentProvider_1.getEnvironmentVariables)();
|
|
1583
|
+
const envVariables = (0, environmentProvider_1.getEnvironmentVariables)(document.uri.fsPath);
|
|
1066
1584
|
const activeEnv = (0, environmentProvider_1.getActiveEnvironment)();
|
|
1067
1585
|
// Merge: env variables first, then file variables (file takes precedence for values)
|
|
1068
1586
|
const allVariables = {};
|
|
@@ -1396,7 +1914,7 @@ class HttpCompletionProvider {
|
|
|
1396
1914
|
getVariablePathCompletions(document, prefix) {
|
|
1397
1915
|
const fullText = document.getText();
|
|
1398
1916
|
const fileVariables = (0, parser_1.extractVariables)(fullText);
|
|
1399
|
-
const envVariables = (0, environmentProvider_1.getEnvironmentVariables)();
|
|
1917
|
+
const envVariables = (0, environmentProvider_1.getEnvironmentVariables)(document.uri.fsPath);
|
|
1400
1918
|
const allVariables = { ...envVariables, ...fileVariables };
|
|
1401
1919
|
const items = [];
|
|
1402
1920
|
const lowerPrefix = prefix.toLowerCase();
|