norn-cli 2.3.0 → 2.4.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.
- package/.claude/skills/norn-social-campaign/SKILL.md +70 -0
- package/CHANGELOG.md +6 -0
- package/demos/nornenv-region-refactor/README.md +64 -0
- package/dist/cli.js +360 -1
- package/out/apiResponseIntellisenseCache.js +394 -0
- package/out/assertionRunner.js +567 -0
- package/out/cacheDir.js +136 -0
- package/out/chatParticipant.js +763 -0
- package/out/cli/colors.js +127 -0
- package/out/cli/formatters/assertion.js +102 -0
- package/out/cli/formatters/index.js +23 -0
- package/out/cli/formatters/response.js +106 -0
- package/out/cli/formatters/summary.js +246 -0
- package/out/cli/redaction.js +237 -0
- package/out/cli/reporters/html.js +689 -0
- package/out/cli/reporters/index.js +22 -0
- package/out/cli/reporters/junit.js +226 -0
- package/out/codeLensProvider.js +351 -0
- package/out/compareContentProvider.js +85 -0
- package/out/completionProvider.js +3739 -0
- package/out/contractAssertionSummary.js +225 -0
- package/out/contractDecorationProvider.js +243 -0
- package/out/coverageCalculator.js +879 -0
- package/out/coveragePanel.js +597 -0
- package/out/debug/breakpointResolver.js +84 -0
- package/out/debug/breakpoints.js +52 -0
- package/out/debug/nornDebugAdapter.js +166 -0
- package/out/debug/nornDebugSession.js +613 -0
- package/out/debug/sequenceLocationIndex.js +77 -0
- package/out/debug/types.js +3 -0
- package/out/deepClone.js +21 -0
- package/out/diagnosticProvider.js +2554 -0
- package/out/environmentParser.js +736 -0
- package/out/environmentProvider.js +544 -0
- package/out/environmentTemplates.js +146 -0
- package/out/errors/formatError.js +113 -0
- package/out/errors/nornError.js +29 -0
- package/out/formUrlEncoded.js +89 -0
- package/out/httpClient.js +348 -0
- package/out/httpRuntimeOptions.js +16 -0
- package/out/importErrors.js +31 -0
- package/out/inlayHintResolver.js +70 -0
- package/out/jsonFileReader.js +323 -0
- package/out/mcpClient.js +193 -0
- package/out/mcpConfig.js +184 -0
- package/out/mcpToolIntellisenseCache.js +96 -0
- package/out/mcpToolSchema.js +50 -0
- package/out/nornConfig.js +132 -0
- package/out/nornHoverProvider.js +124 -0
- package/out/nornInlayHintsProvider.js +191 -0
- package/out/nornPrompt.js +755 -0
- package/out/nornSqlParser.js +286 -0
- package/out/nornapiHoverProvider.js +135 -0
- package/out/nornapiInlayHintsProvider.js +94 -0
- package/out/nornapiParser.js +324 -0
- package/out/nornenvCodeActionProvider.js +101 -0
- package/out/nornenvDecorationProvider.js +239 -0
- package/out/nornenvFoldingProvider.js +63 -0
- package/out/nornenvHoverProvider.js +114 -0
- package/out/nornenvInlayHintsProvider.js +99 -0
- package/out/nornenvLanguageModel.js +187 -0
- package/out/nornenvRegionRefactor.js +267 -0
- package/out/nornsqlHoverProvider.js +95 -0
- package/out/nornsqlInlayHintsProvider.js +114 -0
- package/out/parser.js +839 -0
- package/out/pathAccess.js +28 -0
- package/out/postmanImportPanel.js +732 -0
- package/out/postmanImportPlanner.js +1155 -0
- package/out/postmanImportSidebarView.js +532 -0
- package/out/quotedString.js +35 -0
- package/out/requestPreparation.js +179 -0
- package/out/requestValidation.js +146 -0
- package/out/responsePanel.js +7754 -0
- package/out/schemaGenerator.js +562 -0
- package/out/scriptRunner.js +419 -0
- package/out/secrets/cliSecrets.js +415 -0
- package/out/secrets/crypto.js +105 -0
- package/out/secrets/envFileSecrets.js +177 -0
- package/out/secrets/keyStore.js +259 -0
- package/out/sequenceDeclaration.js +15 -0
- package/out/sequenceRunner.js +3590 -0
- package/out/sqlAdapterRunner.js +122 -0
- package/out/sqlBuiltInAdapters.js +604 -0
- package/out/sqlConfig.js +184 -0
- package/out/starterCatalog.js +554 -0
- package/out/stringUtils.js +25 -0
- package/out/swaggerBodyIntellisenseCache.js +114 -0
- package/out/swaggerParser.js +464 -0
- package/out/testProvider.js +767 -0
- package/out/theoryCaseLoader.js +113 -0
- package/out/validationCache.js +211 -0
- package/package.json +6 -1
|
@@ -0,0 +1,324 @@
|
|
|
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
|
|
@@ -0,0 +1,101 @@
|
|
|
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
|
|
@@ -0,0 +1,239 @@
|
|
|
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
|