norn-cli 2.4.0 → 2.6.0

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.
Files changed (96) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +26 -1
  3. package/dist/cli.js +330 -85
  4. package/package.json +24 -5
  5. package/schemas/norn.config.schema.json +43 -1
  6. package/scripts/__pycache__/reddit_signal_miner.cpython-312.pyc +0 -0
  7. package/scripts/reddit_signal_miner.py +482 -0
  8. package/.claude/settings.local.json +0 -18
  9. package/.claude/skills/norn-social-campaign/SKILL.md +0 -70
  10. package/out/apiResponseIntellisenseCache.js +0 -394
  11. package/out/assertionRunner.js +0 -567
  12. package/out/cacheDir.js +0 -136
  13. package/out/chatParticipant.js +0 -763
  14. package/out/cli/colors.js +0 -127
  15. package/out/cli/formatters/assertion.js +0 -102
  16. package/out/cli/formatters/index.js +0 -23
  17. package/out/cli/formatters/response.js +0 -106
  18. package/out/cli/formatters/summary.js +0 -246
  19. package/out/cli/redaction.js +0 -237
  20. package/out/cli/reporters/html.js +0 -689
  21. package/out/cli/reporters/index.js +0 -22
  22. package/out/cli/reporters/junit.js +0 -226
  23. package/out/codeLensProvider.js +0 -351
  24. package/out/compareContentProvider.js +0 -85
  25. package/out/completionProvider.js +0 -3739
  26. package/out/contractAssertionSummary.js +0 -225
  27. package/out/contractDecorationProvider.js +0 -243
  28. package/out/coverageCalculator.js +0 -879
  29. package/out/coveragePanel.js +0 -597
  30. package/out/debug/breakpointResolver.js +0 -84
  31. package/out/debug/breakpoints.js +0 -52
  32. package/out/debug/nornDebugAdapter.js +0 -166
  33. package/out/debug/nornDebugSession.js +0 -613
  34. package/out/debug/sequenceLocationIndex.js +0 -77
  35. package/out/debug/types.js +0 -3
  36. package/out/deepClone.js +0 -21
  37. package/out/diagnosticProvider.js +0 -2554
  38. package/out/environmentParser.js +0 -736
  39. package/out/environmentProvider.js +0 -544
  40. package/out/environmentTemplates.js +0 -146
  41. package/out/errors/formatError.js +0 -113
  42. package/out/errors/nornError.js +0 -29
  43. package/out/formUrlEncoded.js +0 -89
  44. package/out/httpClient.js +0 -348
  45. package/out/httpRuntimeOptions.js +0 -16
  46. package/out/importErrors.js +0 -31
  47. package/out/inlayHintResolver.js +0 -70
  48. package/out/jsonFileReader.js +0 -323
  49. package/out/mcpClient.js +0 -193
  50. package/out/mcpConfig.js +0 -184
  51. package/out/mcpToolIntellisenseCache.js +0 -96
  52. package/out/mcpToolSchema.js +0 -50
  53. package/out/nornConfig.js +0 -132
  54. package/out/nornHoverProvider.js +0 -124
  55. package/out/nornInlayHintsProvider.js +0 -191
  56. package/out/nornPrompt.js +0 -755
  57. package/out/nornSqlParser.js +0 -286
  58. package/out/nornapiHoverProvider.js +0 -135
  59. package/out/nornapiInlayHintsProvider.js +0 -94
  60. package/out/nornapiParser.js +0 -324
  61. package/out/nornenvCodeActionProvider.js +0 -101
  62. package/out/nornenvDecorationProvider.js +0 -239
  63. package/out/nornenvFoldingProvider.js +0 -63
  64. package/out/nornenvHoverProvider.js +0 -114
  65. package/out/nornenvInlayHintsProvider.js +0 -99
  66. package/out/nornenvLanguageModel.js +0 -187
  67. package/out/nornenvRegionRefactor.js +0 -267
  68. package/out/nornsqlHoverProvider.js +0 -95
  69. package/out/nornsqlInlayHintsProvider.js +0 -114
  70. package/out/parser.js +0 -839
  71. package/out/pathAccess.js +0 -28
  72. package/out/postmanImportPanel.js +0 -732
  73. package/out/postmanImportPlanner.js +0 -1155
  74. package/out/postmanImportSidebarView.js +0 -532
  75. package/out/quotedString.js +0 -35
  76. package/out/requestPreparation.js +0 -179
  77. package/out/requestValidation.js +0 -146
  78. package/out/responsePanel.js +0 -7754
  79. package/out/schemaGenerator.js +0 -562
  80. package/out/scriptRunner.js +0 -419
  81. package/out/secrets/cliSecrets.js +0 -415
  82. package/out/secrets/crypto.js +0 -105
  83. package/out/secrets/envFileSecrets.js +0 -177
  84. package/out/secrets/keyStore.js +0 -259
  85. package/out/sequenceDeclaration.js +0 -15
  86. package/out/sequenceRunner.js +0 -3590
  87. package/out/sqlAdapterRunner.js +0 -122
  88. package/out/sqlBuiltInAdapters.js +0 -604
  89. package/out/sqlConfig.js +0 -184
  90. package/out/starterCatalog.js +0 -554
  91. package/out/stringUtils.js +0 -25
  92. package/out/swaggerBodyIntellisenseCache.js +0 -114
  93. package/out/swaggerParser.js +0 -464
  94. package/out/testProvider.js +0 -767
  95. package/out/theoryCaseLoader.js +0 -113
  96. package/out/validationCache.js +0 -211
@@ -1,324 +0,0 @@
1
- "use strict";
2
- /**
3
- * Parser for .nornapi files
4
- *
5
- * Parses header groups and endpoint definitions from .nornapi files.
6
- */
7
- Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.parseNornApiFile = parseNornApiFile;
9
- exports.getHeaderGroup = getHeaderGroup;
10
- exports.getEndpoint = getEndpoint;
11
- exports.resolveEndpointPath = resolveEndpointPath;
12
- exports.resolveHeaderValues = resolveHeaderValues;
13
- exports.isApiRequestLine = isApiRequestLine;
14
- exports.parseApiRequest = parseApiRequest;
15
- const parser_1 = require("./parser");
16
- /**
17
- * Extracts parameter names from an endpoint path
18
- * e.g., /users/{id}/orders/{orderId} -> ['id', 'orderId']
19
- *
20
- * Note: Does NOT extract {{variable}} references (double braces) - only {param} (single braces)
21
- */
22
- function extractPathParameters(path) {
23
- const params = [];
24
- // Match {param} but NOT {{variable}}
25
- // Use negative lookbehind (?<!{) and negative lookahead (?!})
26
- const regex = /(?<!\{)\{([a-zA-Z_][a-zA-Z0-9_]*)\}(?!\})/g;
27
- let match;
28
- while ((match = regex.exec(path)) !== null) {
29
- params.push(match[1]);
30
- }
31
- return params;
32
- }
33
- /**
34
- * Parses a .nornapi file and returns header groups and endpoint definitions
35
- */
36
- function parseNornApiFile(content) {
37
- const lines = content.split('\n');
38
- const headerGroups = [];
39
- const endpoints = [];
40
- let currentHeaderGroup = null;
41
- let inEndpointsBlock = false;
42
- for (let i = 0; i < lines.length; i++) {
43
- const line = lines[i];
44
- const trimmed = line.trim();
45
- // Skip empty lines and comments
46
- if (!trimmed || trimmed.startsWith('#')) {
47
- continue;
48
- }
49
- // Check for headers block start: headers Name
50
- const headersStartMatch = trimmed.match(/^headers\s+([a-zA-Z_][a-zA-Z0-9_]*)$/i);
51
- if (headersStartMatch) {
52
- currentHeaderGroup = {
53
- name: headersStartMatch[1],
54
- headers: {}
55
- };
56
- continue;
57
- }
58
- // Check for headers block end
59
- if (/^end\s+headers$/i.test(trimmed)) {
60
- if (currentHeaderGroup) {
61
- headerGroups.push(currentHeaderGroup);
62
- currentHeaderGroup = null;
63
- }
64
- continue;
65
- }
66
- // Inside headers block - parse header lines
67
- if (currentHeaderGroup) {
68
- const headerMatch = trimmed.match(/^([a-zA-Z][a-zA-Z0-9\-]*)\s*:\s*(.+)$/);
69
- if (headerMatch) {
70
- currentHeaderGroup.headers[headerMatch[1]] = headerMatch[2].trim();
71
- }
72
- continue;
73
- }
74
- // Check for endpoints block start
75
- if (/^endpoints$/i.test(trimmed)) {
76
- inEndpointsBlock = true;
77
- continue;
78
- }
79
- // Check for endpoints block end
80
- if (/^end\s+endpoints$/i.test(trimmed)) {
81
- inEndpointsBlock = false;
82
- continue;
83
- }
84
- // Inside endpoints block - parse endpoint lines
85
- // Format: EndpointName: METHOD /path/{param}
86
- if (inEndpointsBlock) {
87
- const endpointMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(.+)$/i);
88
- if (endpointMatch) {
89
- const path = endpointMatch[3].trim();
90
- endpoints.push({
91
- name: endpointMatch[1],
92
- method: endpointMatch[2].toUpperCase(),
93
- path,
94
- parameters: extractPathParameters(path)
95
- });
96
- }
97
- continue;
98
- }
99
- }
100
- return { headerGroups, endpoints };
101
- }
102
- /**
103
- * Gets a header group by name (case-sensitive)
104
- */
105
- function getHeaderGroup(definition, name) {
106
- return definition.headerGroups.find(hg => hg.name === name);
107
- }
108
- /**
109
- * Gets an endpoint by name (case-sensitive)
110
- */
111
- function getEndpoint(definition, name) {
112
- return definition.endpoints.find(ep => ep.name === name);
113
- }
114
- /**
115
- * Resolves an endpoint path with provided parameters
116
- * e.g., path: /users/{id}/orders, params: { id: '123' } -> /users/123/orders
117
- */
118
- function resolveEndpointPath(endpoint, params) {
119
- let resolvedPath = endpoint.path;
120
- for (const paramName of endpoint.parameters) {
121
- const value = params[paramName];
122
- if (value !== undefined) {
123
- resolvedPath = resolvedPath.replace(`{${paramName}}`, value);
124
- }
125
- }
126
- return resolvedPath;
127
- }
128
- /**
129
- * Applies variable substitution to header values
130
- */
131
- function resolveHeaderValues(headerGroup, variables) {
132
- const resolved = {};
133
- for (const [name, value] of Object.entries(headerGroup.headers)) {
134
- resolved[name] = (0, parser_1.substituteVariables)(value, variables);
135
- }
136
- return resolved;
137
- }
138
- /**
139
- * Checks if a request line uses endpoint syntax instead of a URL
140
- * Endpoint syntax: METHOD EndpointName(param, ...) [HeaderGroups...]
141
- * URL syntax: METHOD http://... or METHOD /path
142
- *
143
- * Returns true if the URL part starts with a word that matches an endpoint name
144
- */
145
- function isApiRequestLine(requestContent, endpoints) {
146
- const lines = requestContent.split('\n');
147
- // Find the first non-empty, non-comment, non-import line
148
- let firstLine = '';
149
- for (const line of lines) {
150
- const trimmed = line.trim();
151
- if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('import ')) {
152
- firstLine = trimmed;
153
- break;
154
- }
155
- }
156
- if (!firstLine) {
157
- return false;
158
- }
159
- // Match METHOD followed by non-URL pattern
160
- const match = firstLine.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+([a-zA-Z_][a-zA-Z0-9_]*)/i);
161
- if (!match) {
162
- return false;
163
- }
164
- const potentialEndpointName = match[2];
165
- // Check if this matches a known endpoint
166
- return endpoints.some(ep => ep.name === potentialEndpointName);
167
- }
168
- /**
169
- * Parses an API request block (endpoint-based syntax)
170
- *
171
- * Handles two syntax styles:
172
- * 1. Single line: GET GetUser("123") Auth Json
173
- * 2. Multi-line:
174
- * GET GetUser("123")
175
- * Auth
176
- * Json
177
- *
178
- * Returns parsed information or undefined if not an API request
179
- */
180
- function parseApiRequest(requestContent, endpoints, headerGroups) {
181
- const lines = requestContent.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#') && !l.startsWith('import '));
182
- if (lines.length === 0) {
183
- return undefined;
184
- }
185
- const firstLine = lines[0];
186
- // Parse: METHOD EndpointName(params...) [HeaderGroups...]
187
- // Params can be: positional values or key=value pairs
188
- const lineMatch = firstLine.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+([a-zA-Z_][a-zA-Z0-9_]*)(?:\(([^)]*)\))?\s*(.*)$/i);
189
- if (!lineMatch) {
190
- return undefined;
191
- }
192
- const methodFromLine = lineMatch[1].toUpperCase();
193
- const endpointName = lineMatch[2];
194
- const paramsStr = lineMatch[3] || '';
195
- const headerGroupsOnLine = lineMatch[4]?.trim() || '';
196
- // Find the endpoint
197
- const endpoint = endpoints.find(ep => ep.name === endpointName);
198
- if (!endpoint) {
199
- return undefined;
200
- }
201
- // Verify the method matches (or allow method from line to override)
202
- // Actually, the user might use a different method - we use the line's method
203
- const method = methodFromLine;
204
- // Parse parameters: positional or key=value
205
- const params = {};
206
- if (paramsStr) {
207
- // Split by comma, respecting quotes
208
- const paramTokens = parseParamTokens(paramsStr);
209
- // Check if key=value or positional
210
- if (paramTokens.length > 0 && paramTokens[0].includes(':')) {
211
- // Key=value format: GetUser(id: "123")
212
- for (const token of paramTokens) {
213
- const kvMatch = token.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.+)$/);
214
- if (kvMatch) {
215
- params[kvMatch[1]] = unquote(kvMatch[2].trim());
216
- }
217
- }
218
- }
219
- else {
220
- // Positional format: GetUser("123") -> map to endpoint parameters in order
221
- for (let i = 0; i < paramTokens.length && i < endpoint.parameters.length; i++) {
222
- params[endpoint.parameters[i]] = unquote(paramTokens[i].trim());
223
- }
224
- }
225
- }
226
- // Collect header group names and inline headers
227
- const headerGroupNames = [];
228
- const inlineHeaders = {};
229
- // From same line
230
- if (headerGroupsOnLine) {
231
- const names = headerGroupsOnLine.split(/\s+/).filter(n => n);
232
- for (const name of names) {
233
- if (headerGroups.some(hg => hg.name === name)) {
234
- headerGroupNames.push(name);
235
- }
236
- }
237
- }
238
- // From subsequent lines:
239
- // - Header group names (single word matching a defined group)
240
- // - Inline headers (HeaderName: value format)
241
- // - Body content (everything else after headers section)
242
- const bodyLines = [];
243
- let inBodySection = false;
244
- for (let i = 1; i < lines.length; i++) {
245
- const line = lines[i];
246
- // Once we start the body section, everything goes to body
247
- if (inBodySection) {
248
- bodyLines.push(line);
249
- continue;
250
- }
251
- // Check if this line is a header group name
252
- if (headerGroups.some(hg => hg.name === line)) {
253
- headerGroupNames.push(line);
254
- continue;
255
- }
256
- // Check if this is an inline header (HeaderName: value)
257
- const headerMatch = line.match(/^([a-zA-Z][a-zA-Z0-9\-]*)\s*:\s*(.+)$/);
258
- if (headerMatch) {
259
- let headerValue = headerMatch[2].trim();
260
- // Remove surrounding quotes if present
261
- if ((headerValue.startsWith('"') && headerValue.endsWith('"')) ||
262
- (headerValue.startsWith("'") && headerValue.endsWith("'"))) {
263
- headerValue = headerValue.slice(1, -1);
264
- }
265
- inlineHeaders[headerMatch[1]] = headerValue;
266
- continue;
267
- }
268
- // Not a header group or inline header - start of body section
269
- inBodySection = true;
270
- bodyLines.push(line);
271
- }
272
- const body = bodyLines.length > 0 ? bodyLines.join('\n') : undefined;
273
- return {
274
- method,
275
- endpointName,
276
- params,
277
- headerGroupNames,
278
- inlineHeaders,
279
- body
280
- };
281
- }
282
- /**
283
- * Splits parameter string by commas, respecting quotes
284
- */
285
- function parseParamTokens(paramsStr) {
286
- const tokens = [];
287
- let current = '';
288
- let inQuote = false;
289
- let quoteChar = '';
290
- for (const char of paramsStr) {
291
- if ((char === '"' || char === "'") && !inQuote) {
292
- inQuote = true;
293
- quoteChar = char;
294
- current += char;
295
- }
296
- else if (char === quoteChar && inQuote) {
297
- inQuote = false;
298
- quoteChar = '';
299
- current += char;
300
- }
301
- else if (char === ',' && !inQuote) {
302
- tokens.push(current.trim());
303
- current = '';
304
- }
305
- else {
306
- current += char;
307
- }
308
- }
309
- if (current.trim()) {
310
- tokens.push(current.trim());
311
- }
312
- return tokens;
313
- }
314
- /**
315
- * Removes surrounding quotes from a string value
316
- */
317
- function unquote(value) {
318
- if ((value.startsWith('"') && value.endsWith('"')) ||
319
- (value.startsWith("'") && value.endsWith("'"))) {
320
- return value.slice(1, -1);
321
- }
322
- return value;
323
- }
324
- //# sourceMappingURL=nornapiParser.js.map
@@ -1,101 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.NornenvCodeActionProvider = void 0;
37
- const vscode = __importStar(require("vscode"));
38
- const nornenvLanguageModel_1 = require("./nornenvLanguageModel");
39
- function levenshteinDistance(left, right) {
40
- const a = left.toLowerCase();
41
- const b = right.toLowerCase();
42
- const previous = Array.from({ length: b.length + 1 }, (_, index) => index);
43
- const current = Array.from({ length: b.length + 1 }, () => 0);
44
- for (let i = 1; i <= a.length; i++) {
45
- current[0] = i;
46
- for (let j = 1; j <= b.length; j++) {
47
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
48
- current[j] = Math.min(current[j - 1] + 1, previous[j] + 1, previous[j - 1] + cost);
49
- }
50
- for (let j = 0; j <= b.length; j++) {
51
- previous[j] = current[j];
52
- }
53
- }
54
- return previous[b.length];
55
- }
56
- function closestName(name, candidates) {
57
- let best;
58
- let bestDistance = Number.POSITIVE_INFINITY;
59
- for (const candidate of candidates) {
60
- const distance = levenshteinDistance(name, candidate);
61
- if (distance < bestDistance) {
62
- bestDistance = distance;
63
- best = candidate;
64
- }
65
- }
66
- return bestDistance <= (name.length <= 5 ? 1 : 2) ? best : undefined;
67
- }
68
- class NornenvCodeActionProvider {
69
- static providedCodeActionKinds = [vscode.CodeActionKind.QuickFix];
70
- provideCodeActions(document, _range, context) {
71
- const actions = [];
72
- const unknownParentDiagnostics = context.diagnostics.filter(diagnostic => diagnostic.code === 'unknown-extends-parent');
73
- if (unknownParentDiagnostics.length === 0) {
74
- return actions;
75
- }
76
- let candidates;
77
- try {
78
- const filePath = document.uri.scheme === 'file' ? document.uri.fsPath : undefined;
79
- const config = (0, nornenvLanguageModel_1.loadResolvedNornenvConfigFromText)(document.getText(), filePath);
80
- candidates = config.templates.map(template => template.name);
81
- }
82
- catch {
83
- return actions;
84
- }
85
- for (const diagnostic of unknownParentDiagnostics) {
86
- const unknownName = document.getText(diagnostic.range);
87
- const suggestion = closestName(unknownName, candidates);
88
- if (!suggestion) {
89
- continue;
90
- }
91
- const action = new vscode.CodeAction(`Replace '${unknownName}' with '${suggestion}'`, vscode.CodeActionKind.QuickFix);
92
- action.diagnostics = [diagnostic];
93
- action.edit = new vscode.WorkspaceEdit();
94
- action.edit.replace(document.uri, diagnostic.range, suggestion);
95
- actions.push(action);
96
- }
97
- return actions;
98
- }
99
- }
100
- exports.NornenvCodeActionProvider = NornenvCodeActionProvider;
101
- //# sourceMappingURL=nornenvCodeActionProvider.js.map
@@ -1,239 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.NornenvDecorationProvider = void 0;
37
- const vscode = __importStar(require("vscode"));
38
- const environmentParser_1 = require("./environmentParser");
39
- const environmentProvider_1 = require("./environmentProvider");
40
- const nornenvLanguageModel_1 = require("./nornenvLanguageModel");
41
- // Norn brand palette — keep in sync with `norn_website/src/styles/global.css`
42
- // (`.file-pill.norn / .nornsql / .nornenv`) so editor chips match website chips.
43
- const BRAND_TEAL = '#4ec9b0';
44
- const BRAND_BLUE = '#569cd6';
45
- const BRAND_AMBER = '#d7ba7d';
46
- const BRAND_PURPLE = '#c586c0';
47
- const BRAND_TEAL_SUBTLE = 'rgba(78, 201, 176, 0.15)';
48
- const BRAND_TEAL_BORDER = 'rgba(78, 201, 176, 0.35)';
49
- const BRAND_AMBER_SUBTLE = 'rgba(215, 186, 125, 0.15)';
50
- const BRAND_AMBER_BORDER = 'rgba(215, 186, 125, 0.35)';
51
- const BRAND_PURPLE_SUBTLE = 'rgba(197, 134, 192, 0.15)';
52
- const BRAND_PURPLE_BORDER = 'rgba(197, 134, 192, 0.35)';
53
- /**
54
- * Builds an `after.contentText` decoration with proper margin spacing for the *first*
55
- * chip on a line, and (for the rest) chained tightly so each new chip just adds a
56
- * separator dot and the chip text.
57
- *
58
- * VS Code does not let us set per-chip background colors via a single
59
- * `after.contentText`, so each chip kind gets its own `TextEditorDecorationType`.
60
- * Multiple decorations stack at the end of the line in registration order.
61
- */
62
- class NornenvDecorationProvider {
63
- countChipDecoration;
64
- inheritedChipDecoration;
65
- overridesChipDecoration;
66
- activeChipDecoration;
67
- cycleChipDecoration;
68
- unknownParentChipDecoration;
69
- envParentChipDecoration;
70
- environmentSubscription;
71
- constructor() {
72
- // `N vars` — neutral muted count.
73
- // Leading spacing comes from per-instance contentText so the *first* non-pill
74
- // chip on a line gets the gap regardless of which kind it happens to be.
75
- // (The `extends X, Y` chip used to live here; it was pure noise because the same
76
- // text already appears in the section header immediately to the left.)
77
- this.countChipDecoration = vscode.window.createTextEditorDecorationType({
78
- after: {
79
- color: new vscode.ThemeColor('descriptionForeground'),
80
- fontStyle: 'italic'
81
- }
82
- });
83
- // `N inherited` — brand blue (matches website's .nornapi pill text)
84
- this.inheritedChipDecoration = vscode.window.createTextEditorDecorationType({
85
- after: {
86
- color: BRAND_BLUE,
87
- fontStyle: 'italic'
88
- }
89
- });
90
- // `N overrides` — brand teal (the "self-contribution" color)
91
- this.overridesChipDecoration = vscode.window.createTextEditorDecorationType({
92
- after: {
93
- color: BRAND_TEAL,
94
- fontStyle: 'italic'
95
- }
96
- });
97
- // Active pill — subtle teal-on-teal (.file-pill.norn from the website).
98
- // The `border` field's value is serialized verbatim into the inline `style`
99
- // attribute, so we smuggle padding + border-radius through trailing semicolons.
100
- this.activeChipDecoration = this.createSubtlePillDecoration(BRAND_TEAL, BRAND_TEAL_SUBTLE, BRAND_TEAL_BORDER);
101
- // Error pills — subtle purple-on-purple (.file-pill.nornenv): cycle, env-as-parent
102
- this.cycleChipDecoration = this.createSubtlePillDecoration(BRAND_PURPLE, BRAND_PURPLE_SUBTLE, BRAND_PURPLE_BORDER);
103
- this.envParentChipDecoration = this.createSubtlePillDecoration(BRAND_PURPLE, BRAND_PURPLE_SUBTLE, BRAND_PURPLE_BORDER);
104
- // Warning pill — subtle amber-on-amber (.file-pill.nornsql): unknown parent
105
- this.unknownParentChipDecoration = this.createSubtlePillDecoration(BRAND_AMBER, BRAND_AMBER_SUBTLE, BRAND_AMBER_BORDER);
106
- this.environmentSubscription = (0, environmentProvider_1.onDidChangeActiveEnvironment)(() => {
107
- this.updateVisibleEditors();
108
- });
109
- }
110
- /**
111
- * Pastel pill matching the website's `.file-pill.<lang>` styling:
112
- * subtle-tinted background + colored text + 35%-alpha border, fully rounded.
113
- * `font-weight: 500` to match the website pill's weight.
114
- */
115
- createSubtlePillDecoration(textColor, backgroundColor, borderColor) {
116
- return vscode.window.createTextEditorDecorationType({
117
- after: {
118
- color: textColor,
119
- backgroundColor,
120
- border: `1px solid ${borderColor}; border-radius: 100px; padding: 0 8px; margin: 0 0 0 8px; font-weight: 500;`,
121
- fontStyle: 'normal'
122
- }
123
- });
124
- }
125
- updateVisibleEditors() {
126
- for (const editor of vscode.window.visibleTextEditors) {
127
- this.update(editor);
128
- }
129
- }
130
- update(editor) {
131
- if (!editor || (editor.document.languageId !== 'nornenv' && !editor.document.uri.fsPath.endsWith('.nornenv'))) {
132
- return;
133
- }
134
- try {
135
- const text = editor.document.getText();
136
- const filePath = editor.document.uri.scheme === 'file' ? editor.document.uri.fsPath : undefined;
137
- const config = (0, nornenvLanguageModel_1.loadResolvedNornenvConfigFromText)(text, filePath);
138
- const model = (0, nornenvLanguageModel_1.parseNornenvDocumentModel)(text);
139
- const activeEnvironment = (0, environmentProvider_1.getActiveEnvironment)(filePath);
140
- const cycles = new Set((0, environmentParser_1.detectExtendsCycles)(config).flatMap(cycle => cycle.path));
141
- const sectionChips = model.sections.map(section => ({
142
- section,
143
- chips: this.buildChips(section, config, cycles, activeEnvironment)
144
- }));
145
- // For each chip kind, build the DecorationOptions[] for the lines that need it.
146
- // Each chip on a line gets a separator dot from the previous chip's text — except
147
- // the first chip in this line, which gets a leading margin via the decoration type.
148
- const optionsByKind = {
149
- count: [],
150
- inherited: [],
151
- overrides: [],
152
- active: [],
153
- cycle: [],
154
- 'unknown-parent': [],
155
- 'env-parent': []
156
- };
157
- for (const { section, chips } of sectionChips) {
158
- const line = editor.document.lineAt(section.lineNumber);
159
- const endOfLine = new vscode.Range(section.lineNumber, line.text.length, section.lineNumber, line.text.length);
160
- for (let index = 0; index < chips.length; index++) {
161
- const chip = chips[index];
162
- const isFirstOnLine = index === 0;
163
- // Pills (active / warnings) carry their own left margin via the border CSS
164
- // hack, so they never need the separator dot. Non-pill chips need a leading
165
- // gap when they're the first on the line — otherwise they slam into `]`.
166
- const isPill = chip.kind === 'active' || chip.kind === 'cycle' || chip.kind === 'unknown-parent' || chip.kind === 'env-parent';
167
- const separator = isFirstOnLine ? ' ' : ' · ';
168
- const contentText = isPill ? chip.text : `${separator}${chip.text}`;
169
- optionsByKind[chip.kind].push({
170
- range: endOfLine,
171
- renderOptions: {
172
- after: { contentText }
173
- }
174
- });
175
- }
176
- }
177
- editor.setDecorations(this.countChipDecoration, optionsByKind.count);
178
- editor.setDecorations(this.inheritedChipDecoration, optionsByKind.inherited);
179
- editor.setDecorations(this.overridesChipDecoration, optionsByKind.overrides);
180
- editor.setDecorations(this.activeChipDecoration, optionsByKind.active);
181
- editor.setDecorations(this.cycleChipDecoration, optionsByKind.cycle);
182
- editor.setDecorations(this.unknownParentChipDecoration, optionsByKind['unknown-parent']);
183
- editor.setDecorations(this.envParentChipDecoration, optionsByKind['env-parent']);
184
- }
185
- catch {
186
- const emptyDecorations = [];
187
- editor.setDecorations(this.countChipDecoration, emptyDecorations);
188
- editor.setDecorations(this.inheritedChipDecoration, emptyDecorations);
189
- editor.setDecorations(this.overridesChipDecoration, emptyDecorations);
190
- editor.setDecorations(this.activeChipDecoration, emptyDecorations);
191
- editor.setDecorations(this.cycleChipDecoration, emptyDecorations);
192
- editor.setDecorations(this.unknownParentChipDecoration, emptyDecorations);
193
- editor.setDecorations(this.envParentChipDecoration, emptyDecorations);
194
- }
195
- }
196
- buildChips(section, config, cycles, activeEnvironment) {
197
- const found = (0, environmentParser_1.findExtendsNode)(section.name, config);
198
- const ownVariables = found ? Object.keys(found.node.variables) : [];
199
- const inherited = (0, environmentParser_1.resolveInheritedVariableDetails)(section.name, config);
200
- const ancestorOrCommonNames = new Set([
201
- ...Object.keys(config.common),
202
- ...(0, environmentParser_1.collectAncestorVariableNames)(section.name, config)
203
- ]);
204
- const overrides = ownVariables.filter(name => ancestorOrCommonNames.has(name)).length;
205
- const chips = [];
206
- chips.push({ kind: 'count', text: `${ownVariables.length} vars` });
207
- if (inherited.size > 0) {
208
- chips.push({ kind: 'inherited', text: `${inherited.size} inherited` });
209
- }
210
- if (overrides > 0) {
211
- chips.push({ kind: 'overrides', text: `${overrides} overrides` });
212
- }
213
- if (section.kind === 'env' && section.name === activeEnvironment) {
214
- chips.push({ kind: 'active', text: 'Active' });
215
- }
216
- if (section.parents.some(parent => config.environments.some(env => env.name === parent) && !(0, environmentParser_1.findExtendsTemplate)(parent, config))) {
217
- chips.push({ kind: 'env-parent', text: '! env parent' });
218
- }
219
- else if (section.parents.some(parent => !(0, environmentParser_1.findExtendsTemplate)(parent, config))) {
220
- chips.push({ kind: 'unknown-parent', text: '! unknown parent' });
221
- }
222
- if (cycles.has(section.name)) {
223
- chips.push({ kind: 'cycle', text: '! cycle' });
224
- }
225
- return chips;
226
- }
227
- dispose() {
228
- this.environmentSubscription.dispose();
229
- this.countChipDecoration.dispose();
230
- this.inheritedChipDecoration.dispose();
231
- this.overridesChipDecoration.dispose();
232
- this.activeChipDecoration.dispose();
233
- this.cycleChipDecoration.dispose();
234
- this.unknownParentChipDecoration.dispose();
235
- this.envParentChipDecoration.dispose();
236
- }
237
- }
238
- exports.NornenvDecorationProvider = NornenvDecorationProvider;
239
- //# sourceMappingURL=nornenvDecorationProvider.js.map