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.
@@ -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
- // Check if user might be typing a JSON variable name for property assignment
144
- const jsonVarCompletions = this.getJsonVariableCompletions(document, linePrefix);
145
- // If we have JSON variable completions, show those (for property assignment)
146
- if (jsonVarCompletions.length > 0) {
147
- return jsonVarCompletions;
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 (this.isAfterApiRequest(document, position)) {
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 API request (METHOD EndpointName).
960
- * This is used to provide header completions for API endpoint requests.
961
- * Allows inline headers and header groups on lines following an API endpoint.
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 (apiDefs.endpoints.length === 0) {
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
- // Check if this line is an API request (METHOD EndpointName)
985
- const match = prevLine.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+([a-zA-Z_][a-zA-Z0-9_]*)(?:\([^)]*\))?/i);
986
- if (match) {
987
- const endpointName = match[2];
988
- if (apiDefs.endpoints.some(ep => ep.name === endpointName)) {
989
- return true;
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();