cluaupp 1.4.0 → 1.5.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 (34) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +2 -2
  3. package/editors/vscode/README.md +17 -0
  4. package/editors/vscode/extension.js +140 -21
  5. package/editors/vscode/icons/flare.svg +5 -0
  6. package/editors/vscode/language-configuration.json +21 -0
  7. package/editors/vscode/package.json +61 -7
  8. package/editors/vscode/snippets/flare.json +27 -0
  9. package/editors/vscode/syntaxes/flare.tmLanguage.json +75 -0
  10. package/examples/game/.vscode/extensions.json +3 -1
  11. package/examples/game/.vscode/settings.json +2 -1
  12. package/examples/game/include/clpp/libs/roster.clh +6 -0
  13. package/examples/game/src/ReplicatedStorage/Shared/Net/Net.flare +5 -0
  14. package/examples/game/src/ServerScriptService/Boot/DataBoot.server.clpp +1 -1
  15. package/generated/cli.js +3 -3
  16. package/generated/flare/index.js +15 -3
  17. package/generated/flare/language.js +561 -0
  18. package/generated/flare/parse.js +7 -6
  19. package/generated/intellisense.js +20 -2
  20. package/generated/lsp.js +118 -7
  21. package/include/clpp/libs/roster.clh +6 -0
  22. package/package.json +1 -1
  23. package/runtime/Roster/init.luau +6 -0
  24. package/src/cli.ts +3 -3
  25. package/src/flare/index.ts +20 -1
  26. package/src/flare/language.ts +708 -0
  27. package/src/flare/parse.ts +6 -6
  28. package/src/intellisense.ts +19 -2
  29. package/src/lsp.ts +151 -8
  30. package/templates/game/.vscode/extensions.json +3 -1
  31. package/templates/game/.vscode/settings.json +2 -1
  32. package/templates/game/include/clpp/libs/roster.clh +6 -0
  33. package/templates/game/src/ReplicatedStorage/Shared/Net/Net.flare +5 -0
  34. package/templates/game/src/ServerScriptService/Boot/DataBoot.server.clpp +1 -1
@@ -0,0 +1,561 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FLARE_TYPE_DOCS = exports.FLARE_CANONICAL_TYPES = void 0;
4
+ exports.analyzeFlare = analyzeFlare;
5
+ exports.diagnoseFlare = diagnoseFlare;
6
+ exports.hoverAt = hoverAt;
7
+ exports.completionsAt = completionsAt;
8
+ exports.symbolsIn = symbolsIn;
9
+ exports.signatureAt = signatureAt;
10
+ exports.isFlareDocument = isFlareDocument;
11
+ const parse_js_1 = require("./parse.js");
12
+ const IDENT_RE = new RegExp(`^${parse_js_1.FLARE_IDENT}$`);
13
+ const WORD_RE = /[A-Za-z_][A-Za-z0-9_]*/g;
14
+ exports.FLARE_CANONICAL_TYPES = [
15
+ "u8",
16
+ "u16",
17
+ "u32",
18
+ "i8",
19
+ "i16",
20
+ "i32",
21
+ "f32",
22
+ "f64",
23
+ "bool",
24
+ "string",
25
+ "Vector3",
26
+ "Vector2",
27
+ "CFrame",
28
+ "Color3",
29
+ "UDim",
30
+ "UDim2",
31
+ "BrickColor",
32
+ "buffer",
33
+ "Instance",
34
+ "Player",
35
+ ];
36
+ exports.FLARE_TYPE_DOCS = {
37
+ u8: "Unsigned 8-bit integer (0–255). Packet/query ids use this.",
38
+ u16: "Unsigned 16-bit integer.",
39
+ u32: "Unsigned 32-bit integer.",
40
+ i8: "Signed 8-bit integer.",
41
+ i16: "Signed 16-bit integer.",
42
+ i32: "Signed 32-bit integer. Alias: `int`.",
43
+ int: "Alias for `i32`.",
44
+ f32: "32-bit float. Alias: `float`.",
45
+ float: "Alias for `f32`.",
46
+ f64: "64-bit float. Aliases: `double`, `number`. Use for `tick()`.",
47
+ double: "Alias for `f64`.",
48
+ number: "Alias for `f64`.",
49
+ bool: "Boolean. Alias: `boolean`.",
50
+ boolean: "Alias for `bool`.",
51
+ string: "UTF-8 string (length-prefixed on the wire).",
52
+ Vector3: "Roblox Vector3 (three f32s).",
53
+ Vector2: "Roblox Vector2 (two f32s).",
54
+ CFrame: "Roblox CFrame.",
55
+ Color3: "Roblox Color3.",
56
+ UDim: "Roblox UDim.",
57
+ UDim2: "Roblox UDim2.",
58
+ BrickColor: "Roblox BrickColor.",
59
+ buffer: "Luau buffer payload.",
60
+ Instance: "Roblox Instance (sidecar array, not packed in the buffer).",
61
+ Player: "Roblox Player instance (sidecar). On `from Server` `Fire`, the first `Player` is the **recipient**, not a schema field.",
62
+ };
63
+ const TYPE_LABELS = [...new Set([...exports.FLARE_CANONICAL_TYPES, "int", "float", "double", "boolean"])];
64
+ function lineRange(text, fallback = 0) {
65
+ const start = text.search(/\S/);
66
+ if (start < 0) {
67
+ return { start: fallback, end: Math.max(text.length, fallback + 1) };
68
+ }
69
+ return { start, end: text.length };
70
+ }
71
+ function tokenRange(text, token) {
72
+ const start = text.indexOf(token);
73
+ if (start < 0) {
74
+ return lineRange(text);
75
+ }
76
+ return { start, end: start + token.length };
77
+ }
78
+ function splitLines(source) {
79
+ return String(source).replace(/\r\n/g, "\n").split("\n");
80
+ }
81
+ function isBlank(line) {
82
+ const trimmed = line.trim();
83
+ return !trimmed || trimmed.startsWith("#") || trimmed.startsWith("//");
84
+ }
85
+ function parseFieldsLoose(list, line, lineText, diagnostics) {
86
+ const trimmed = list.trim();
87
+ if (!trimmed) {
88
+ return [];
89
+ }
90
+ const fields = [];
91
+ for (const part of trimmed.split(",")) {
92
+ const piece = part.trim();
93
+ if (!piece) {
94
+ continue;
95
+ }
96
+ const match = piece.match(new RegExp(`^(${parse_js_1.FLARE_IDENT})\\s+(${parse_js_1.FLARE_IDENT})$`));
97
+ if (!match) {
98
+ diagnostics.push({
99
+ line,
100
+ ...tokenRange(lineText, piece),
101
+ message: `expected 'Type name' in '${piece}'`,
102
+ severity: "error",
103
+ });
104
+ continue;
105
+ }
106
+ const mapped = parse_js_1.FLARE_TYPE_ALIASES[match[1]];
107
+ if (!mapped) {
108
+ const hint = closestType(match[1]);
109
+ diagnostics.push({
110
+ line,
111
+ ...tokenRange(lineText, match[1]),
112
+ message: hint ? `unknown type '${match[1]}' — did you mean '${hint}'?` : `unknown type '${match[1]}'`,
113
+ severity: "error",
114
+ });
115
+ continue;
116
+ }
117
+ fields.push({ type: mapped, name: match[2] });
118
+ }
119
+ return fields;
120
+ }
121
+ function closestType(raw) {
122
+ const lower = raw.toLowerCase();
123
+ let best = null;
124
+ let bestDist = 3;
125
+ for (const label of TYPE_LABELS) {
126
+ const dist = levenshtein(lower, label.toLowerCase());
127
+ if (dist < bestDist) {
128
+ bestDist = dist;
129
+ best = label;
130
+ }
131
+ }
132
+ return best;
133
+ }
134
+ function levenshtein(a, b) {
135
+ if (a === b) {
136
+ return 0;
137
+ }
138
+ const rows = a.length + 1;
139
+ const cols = b.length + 1;
140
+ const grid = Array.from({ length: rows }, () => new Array(cols).fill(0));
141
+ for (let i = 0; i < rows; i++) {
142
+ grid[i][0] = i;
143
+ }
144
+ for (let j = 0; j < cols; j++) {
145
+ grid[0][j] = j;
146
+ }
147
+ for (let i = 1; i < rows; i++) {
148
+ for (let j = 1; j < cols; j++) {
149
+ grid[i][j] = Math.min(grid[i - 1][j] + 1, grid[i][j - 1] + 1, grid[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
150
+ }
151
+ }
152
+ return grid[a.length][b.length];
153
+ }
154
+ function parseSideLoose(raw, line, lineText, diagnostics) {
155
+ const side = raw.toLowerCase();
156
+ if (side === "client") {
157
+ return "Client";
158
+ }
159
+ if (side === "server") {
160
+ return "Server";
161
+ }
162
+ diagnostics.push({
163
+ line,
164
+ ...tokenRange(lineText, raw),
165
+ message: `from must be Client or Server, got '${raw}'`,
166
+ severity: "error",
167
+ });
168
+ return null;
169
+ }
170
+ function analyzeFlare(source, filePath = "Net.flare", defaultName = "Net") {
171
+ const diagnostics = [];
172
+ const packets = [];
173
+ const queries = [];
174
+ const seen = new Map();
175
+ let name = defaultName;
176
+ let packetId = 1;
177
+ let queryId = 1;
178
+ const lines = splitLines(source);
179
+ for (let i = 0; i < lines.length; i++) {
180
+ const lineText = lines[i];
181
+ const line = lineText.trim();
182
+ if (isBlank(lineText)) {
183
+ continue;
184
+ }
185
+ const opt = line.match(/^opt\s+name\s*=\s*(.+)$/i);
186
+ if (opt) {
187
+ const value = opt[1].trim().replace(/^["']|["']$/g, "");
188
+ if (!IDENT_RE.test(value)) {
189
+ diagnostics.push({
190
+ line: i,
191
+ ...tokenRange(lineText, opt[1].trim()),
192
+ message: "opt name must be an identifier",
193
+ severity: "error",
194
+ });
195
+ continue;
196
+ }
197
+ name = value;
198
+ continue;
199
+ }
200
+ if (/^opt\b/i.test(line)) {
201
+ diagnostics.push({
202
+ line: i,
203
+ ...lineRange(lineText),
204
+ message: "expected `opt name = Net`",
205
+ severity: "error",
206
+ });
207
+ continue;
208
+ }
209
+ const packet = line.match(new RegExp(`^packet\\s+(${parse_js_1.FLARE_IDENT})\\s*\\((.*)\\)\\s+from\\s+(${parse_js_1.FLARE_IDENT})(?:\\s+(${parse_js_1.FLARE_IDENT}))?\\s*$`, "i"));
210
+ if (packet) {
211
+ const from = parseSideLoose(packet[3], i, lineText, diagnostics);
212
+ const reliability = packet[4];
213
+ if (reliability && !/^(unreliable|reliable)$/i.test(reliability)) {
214
+ diagnostics.push({
215
+ line: i,
216
+ ...tokenRange(lineText, reliability),
217
+ message: `expected unreliable or reliable, got '${reliability}'`,
218
+ severity: "error",
219
+ });
220
+ }
221
+ if (packetId > 255) {
222
+ diagnostics.push({
223
+ line: i,
224
+ ...tokenRange(lineText, packet[1]),
225
+ message: "too many packets (max 255)",
226
+ severity: "error",
227
+ });
228
+ continue;
229
+ }
230
+ const fields = parseFieldsLoose(packet[2], i, lineText, diagnostics);
231
+ if (!from) {
232
+ continue;
233
+ }
234
+ const dup = seen.get(packet[1]);
235
+ if (dup) {
236
+ diagnostics.push({
237
+ line: i,
238
+ ...tokenRange(lineText, packet[1]),
239
+ message: `duplicate ${dup.kind} '${packet[1]}'`,
240
+ severity: "error",
241
+ });
242
+ continue;
243
+ }
244
+ seen.set(packet[1], { line: i, kind: "packet" });
245
+ packets.push({
246
+ kind: "packet",
247
+ name: packet[1],
248
+ id: packetId++,
249
+ from,
250
+ reliable: String(reliability || "reliable").toLowerCase() !== "unreliable",
251
+ fields,
252
+ });
253
+ continue;
254
+ }
255
+ const query = line.match(new RegExp(`^query\\s+(${parse_js_1.FLARE_IDENT})\\s*\\((.*)\\)\\s*->\\s*(${parse_js_1.FLARE_IDENT})\\s*$`, "i"));
256
+ if (query) {
257
+ if (queryId > 255) {
258
+ diagnostics.push({
259
+ line: i,
260
+ ...tokenRange(lineText, query[1]),
261
+ message: "too many queries (max 255)",
262
+ severity: "error",
263
+ });
264
+ continue;
265
+ }
266
+ const returns = parse_js_1.FLARE_TYPE_ALIASES[query[3]];
267
+ if (!returns) {
268
+ const hint = closestType(query[3]);
269
+ diagnostics.push({
270
+ line: i,
271
+ ...tokenRange(lineText, query[3]),
272
+ message: hint ? `unknown type '${query[3]}' — did you mean '${hint}'?` : `unknown type '${query[3]}'`,
273
+ severity: "error",
274
+ });
275
+ }
276
+ const request = parseFieldsLoose(query[2], i, lineText, diagnostics);
277
+ const dup = seen.get(query[1]);
278
+ if (dup) {
279
+ diagnostics.push({
280
+ line: i,
281
+ ...tokenRange(lineText, query[1]),
282
+ message: `duplicate ${dup.kind} '${query[1]}'`,
283
+ severity: "error",
284
+ });
285
+ continue;
286
+ }
287
+ if (!returns) {
288
+ continue;
289
+ }
290
+ seen.set(query[1], { line: i, kind: "query" });
291
+ queries.push({
292
+ kind: "query",
293
+ name: query[1],
294
+ id: queryId++,
295
+ request,
296
+ returns,
297
+ });
298
+ continue;
299
+ }
300
+ diagnostics.push({
301
+ line: i,
302
+ ...lineRange(lineText),
303
+ message: "expected packet Tag(Type name, ...) from Client|Server or query Tag(...) -> Type",
304
+ severity: "error",
305
+ });
306
+ }
307
+ if (packets.length === 0 && queries.length === 0) {
308
+ diagnostics.push({
309
+ line: 0,
310
+ start: 0,
311
+ end: Math.max(lines[0]?.length ?? 1, 1),
312
+ message: "schema is empty — add a packet or query",
313
+ severity: lines.some((line) => !isBlank(line)) ? "error" : "hint",
314
+ });
315
+ }
316
+ return { name, packets, queries, diagnostics };
317
+ }
318
+ function diagnoseFlare(source, filePath) {
319
+ return analyzeFlare(source, filePath).diagnostics;
320
+ }
321
+ function fieldList(fields) {
322
+ return fields.map((field) => `${field.type} ${field.name}`).join(", ");
323
+ }
324
+ function packetApi(packet, ns) {
325
+ const args = fieldList(packet.fields);
326
+ if (packet.from === "Client") {
327
+ return [
328
+ `**packet ${packet.name}** \`${args}\` from Client${packet.reliable ? "" : " unreliable"}`,
329
+ "",
330
+ `Generated \`${ns}.${packet.name}\`:`,
331
+ "- Client: `FireServer(...)`",
332
+ "- Server: `Connect` — first arg is `Player`",
333
+ ].join("\n");
334
+ }
335
+ return [
336
+ `**packet ${packet.name}** \`${args}\` from Server${packet.reliable ? "" : " unreliable"}`,
337
+ "",
338
+ `Generated \`${ns}.${packet.name}\`:`,
339
+ "- Server: `Fire(recipient, …fields)` / `FireAll(…fields)`",
340
+ "- Client: `Connect` — fields only, no recipient",
341
+ ].join("\n");
342
+ }
343
+ function queryApi(query, ns) {
344
+ return [
345
+ `**query ${query.name}** \`${fieldList(query.request)}\` -> ${query.returns}`,
346
+ "",
347
+ `Generated \`${ns}.${query.name}\`:`,
348
+ "- Client: `Invoke(...)`",
349
+ "- Server: `On` — first arg is `Player`, **return** the result",
350
+ ].join("\n");
351
+ }
352
+ function keywordDocs(word) {
353
+ const table = {
354
+ opt: "`opt name = Net` — generated module / header name.",
355
+ name: "Identifier written by `cluaupp build` (`Net.clh`, `Net.Hit`, …).",
356
+ packet: "One-way remote. `from Client` = FireServer; `from Server` = Fire / FireAll.",
357
+ query: "RPC. Client `Invoke`, server `On` must return the result type.",
358
+ from: "`from Client` or `from Server` — who **sends** the packet.",
359
+ Client: "The client sends. Server `Connect` receives `Player` first.",
360
+ Server: "The server sends. `Fire(player, …)` first arg is the recipient.",
361
+ unreliable: "Unreliable datagram (no ordering/retry). Server-origin sparks, not money.",
362
+ reliable: "Default. Ordered, retried. Keep currency off this channel.",
363
+ };
364
+ return table[word] ?? null;
365
+ }
366
+ function wordAt(lineText, character) {
367
+ WORD_RE.lastIndex = 0;
368
+ let match;
369
+ while ((match = WORD_RE.exec(lineText))) {
370
+ const start = match.index;
371
+ const end = start + match[0].length;
372
+ if (character >= start && character <= end) {
373
+ return { word: match[0], start, end };
374
+ }
375
+ }
376
+ return null;
377
+ }
378
+ function hoverAt(source, line, character) {
379
+ const lines = splitLines(source);
380
+ const lineText = lines[line] ?? "";
381
+ const found = wordAt(lineText, character);
382
+ if (!found) {
383
+ return null;
384
+ }
385
+ const analysis = analyzeFlare(source);
386
+ const packet = analysis.packets.find((item) => item.name === found.word);
387
+ if (packet) {
388
+ return { contents: packetApi(packet, analysis.name), start: found.start, end: found.end };
389
+ }
390
+ const query = analysis.queries.find((item) => item.name === found.word);
391
+ if (query) {
392
+ return { contents: queryApi(query, analysis.name), start: found.start, end: found.end };
393
+ }
394
+ const typeDoc = exports.FLARE_TYPE_DOCS[found.word];
395
+ if (typeDoc) {
396
+ const canon = parse_js_1.FLARE_TYPE_ALIASES[found.word] ?? found.word;
397
+ return { contents: `**${found.word}**${canon !== found.word ? ` → \`${canon}\`` : ""}\n\n${typeDoc}`, start: found.start, end: found.end };
398
+ }
399
+ const keyword = keywordDocs(found.word);
400
+ if (keyword) {
401
+ return { contents: keyword, start: found.start, end: found.end };
402
+ }
403
+ return null;
404
+ }
405
+ function item(label, kind, detail, documentation, insertText = label, snippet = false, sortText) {
406
+ return { label, kind, detail, documentation, insertText, snippet, sortText: sortText ?? label };
407
+ }
408
+ function typeItems() {
409
+ return TYPE_LABELS.map((label, index) => item(label, "type", parse_js_1.FLARE_TYPE_ALIASES[label] ?? label, exports.FLARE_TYPE_DOCS[label] ?? "", label, false, `t${String(index).padStart(2, "0")}`));
410
+ }
411
+ const SNIPPETS = [
412
+ item("packet (from Client)", "snippet", "Client → server packet", "FireServer on the client; Connect on the server with Player first.", "packet ${1:Hit}(${2:Player} ${3:target}, ${4:i32} ${5:damage}) from Client", true, "0packetc"),
413
+ item("packet (from Server)", "snippet", "Server → client packet", "Fire(recipient, …fields) / FireAll. Client Connect sees fields only.", "packet ${1:Welcome}(${2:i32} ${3:userId}, ${4:f64} ${5:clock}) from Server", true, "0packets"),
414
+ item("packet (unreliable)", "snippet", "Unreliable server packet", "No retry. Visual sparks, not economy.", "packet ${1:Spark}(${2:Vector3} ${3:pos}) from Server unreliable", true, "0packetu"),
415
+ item("query", "snippet", "RPC query", "Client Invoke; server On returns the result.", "query ${1:Session}() -> ${2:i32}", true, "0query"),
416
+ item("opt name", "snippet", "Generated module name", "Writes Net.clh / Net.luau.", "opt name = ${1:Net}", true, "0opt"),
417
+ ];
418
+ function insideParens(text) {
419
+ const open = text.lastIndexOf("(");
420
+ const close = text.lastIndexOf(")");
421
+ return open >= 0 && open > close;
422
+ }
423
+ function lastParamFragment(text) {
424
+ const open = text.lastIndexOf("(");
425
+ if (open < 0) {
426
+ return "";
427
+ }
428
+ const inner = text.slice(open + 1);
429
+ const parts = inner.split(",");
430
+ return (parts[parts.length - 1] ?? "").trim();
431
+ }
432
+ function completionsAt(source, line, character) {
433
+ const lines = splitLines(source);
434
+ const lineText = lines[line] ?? "";
435
+ const before = lineText.slice(0, character);
436
+ const text = before.trimStart();
437
+ if (!text) {
438
+ return [
439
+ item("packet", "keyword", "One-way remote", keywordDocs("packet") ?? "", "packet "),
440
+ item("query", "keyword", "RPC", keywordDocs("query") ?? "", "query "),
441
+ item("opt", "keyword", "Module name", keywordDocs("opt") ?? "", "opt name = Net"),
442
+ ...SNIPPETS,
443
+ ];
444
+ }
445
+ if (/^opt(\s+\w*)?$/i.test(text)) {
446
+ return [item("name", "field", "opt name = Net", keywordDocs("name") ?? "")];
447
+ }
448
+ if (/^opt\s+name\s*$/i.test(text)) {
449
+ return [item("=", "keyword", "opt name = Net", "Assign the generated identifier.", "= Net")];
450
+ }
451
+ if (/\bfrom\s+(Client|Server)\s+\w*$/i.test(text)) {
452
+ return [
453
+ item("unreliable", "keyword", "Unreliable datagram", keywordDocs("unreliable") ?? ""),
454
+ item("reliable", "keyword", "Ordered retry (default)", keywordDocs("reliable") ?? ""),
455
+ ];
456
+ }
457
+ if (/\bfrom\s+\w*$/i.test(text)) {
458
+ return [
459
+ item("Client", "value", "from Client", keywordDocs("Client") ?? ""),
460
+ item("Server", "value", "from Server", keywordDocs("Server") ?? ""),
461
+ ];
462
+ }
463
+ if (/^packet\b.*\)\s+$/i.test(text) || /^packet\b.*\)\s+\w*$/i.test(text)) {
464
+ return [item("from", "keyword", "from Client | Server", keywordDocs("from") ?? "", "from ")];
465
+ }
466
+ if (/->\s*\w*$/.test(text)) {
467
+ return typeItems();
468
+ }
469
+ if ((/^packet\b/i.test(text) || /^query\b/i.test(text)) && insideParens(text)) {
470
+ const fragment = lastParamFragment(text);
471
+ if (!fragment) {
472
+ return typeItems();
473
+ }
474
+ const bits = fragment.split(/\s+/);
475
+ if (bits.length === 1) {
476
+ const mapped = parse_js_1.FLARE_TYPE_ALIASES[bits[0]];
477
+ if (mapped) {
478
+ return [
479
+ item("name", "field", "Field identifier", "Second token is the field name (`i32 damage`).", "${1:value}", true),
480
+ ];
481
+ }
482
+ return typeItems();
483
+ }
484
+ return [];
485
+ }
486
+ if (/^packet\s+$/i.test(text) || /^query\s+$/i.test(text)) {
487
+ return [];
488
+ }
489
+ return SNIPPETS.filter((snippet) => snippet.label.toLowerCase().includes(text.toLowerCase().split(/\s+/)[0] ?? ""));
490
+ }
491
+ function symbolsIn(source) {
492
+ const analysis = analyzeFlare(source);
493
+ const lines = splitLines(source);
494
+ const symbols = [];
495
+ if (analysis.name) {
496
+ const optLine = lines.findIndex((line) => /^\s*opt\s+name\s*=/i.test(line));
497
+ if (optLine >= 0) {
498
+ symbols.push({
499
+ name: analysis.name,
500
+ detail: "opt name",
501
+ kind: "option",
502
+ line: optLine,
503
+ ...lineRange(lines[optLine]),
504
+ });
505
+ }
506
+ }
507
+ for (const packet of analysis.packets) {
508
+ const line = lines.findIndex((row) => new RegExp(`^\\s*packet\\s+${packet.name}\\b`).test(row));
509
+ if (line < 0) {
510
+ continue;
511
+ }
512
+ symbols.push({
513
+ name: packet.name,
514
+ detail: `from ${packet.from}${packet.reliable ? "" : " unreliable"}`,
515
+ kind: "packet",
516
+ line,
517
+ ...tokenRange(lines[line], packet.name),
518
+ });
519
+ }
520
+ for (const query of analysis.queries) {
521
+ const line = lines.findIndex((row) => new RegExp(`^\\s*query\\s+${query.name}\\b`).test(row));
522
+ if (line < 0) {
523
+ continue;
524
+ }
525
+ symbols.push({
526
+ name: query.name,
527
+ detail: `-> ${query.returns}`,
528
+ kind: "query",
529
+ line,
530
+ ...tokenRange(lines[line], query.name),
531
+ });
532
+ }
533
+ return symbols;
534
+ }
535
+ function signatureAt(source, line, character) {
536
+ const lines = splitLines(source);
537
+ const lineText = lines[line] ?? "";
538
+ const before = lineText.slice(0, character);
539
+ const packet = before.match(/packet\s+(\w+)\s*\(([^)]*)$/i);
540
+ const query = before.match(/query\s+(\w+)\s*\(([^)]*)$/i);
541
+ const match = packet || query;
542
+ if (!match) {
543
+ return null;
544
+ }
545
+ const inner = match[2] ?? "";
546
+ const activeParameter = inner.split(",").length - 1;
547
+ return {
548
+ label: `${packet ? "packet" : "query"} ${match[1]}(Type name, …)`,
549
+ documentation: packet
550
+ ? "Each field is `Type name`. Then `from Client` or `from Server`."
551
+ : "Each field is `Type name`. Then `-> Type` for the return.",
552
+ parameters: [
553
+ { label: "Type name", documentation: "Wire type then identifier, e.g. `i32 damage`." },
554
+ { label: "…", documentation: "More fields separated by commas." },
555
+ ],
556
+ activeParameter: Math.max(0, activeParameter),
557
+ };
558
+ }
559
+ function isFlareDocument(uri) {
560
+ return /\.flare$/i.test(uri.split("?")[0] ?? "");
561
+ }
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FLARE_IDENT = exports.FLARE_TYPE_ALIASES = void 0;
3
4
  exports.parseFlare = parseFlare;
4
- const TYPES = {
5
+ exports.FLARE_TYPE_ALIASES = {
5
6
  u8: "u8",
6
7
  u16: "u16",
7
8
  u32: "u32",
@@ -28,12 +29,12 @@ const TYPES = {
28
29
  Instance: "Instance",
29
30
  Player: "Player",
30
31
  };
31
- const IDENT = "[A-Za-z_][A-Za-z0-9_]*";
32
+ exports.FLARE_IDENT = "[A-Za-z_][A-Za-z0-9_]*";
32
33
  function fail(file, line, message) {
33
34
  throw new Error(`cluaupp flare: ${file}:${line}: ${message}`);
34
35
  }
35
36
  function parseType(raw, file, line) {
36
- const mapped = TYPES[raw];
37
+ const mapped = exports.FLARE_TYPE_ALIASES[raw];
37
38
  if (!mapped) {
38
39
  fail(file, line, `unknown type '${raw}'`);
39
40
  }
@@ -50,7 +51,7 @@ function parseFields(list, file, line) {
50
51
  if (!piece) {
51
52
  continue;
52
53
  }
53
- const match = piece.match(new RegExp(`^(${IDENT})\\s+(${IDENT})$`));
54
+ const match = piece.match(new RegExp(`^(${exports.FLARE_IDENT})\\s+(${exports.FLARE_IDENT})$`));
54
55
  if (!match) {
55
56
  fail(file, line, `expected 'Type name' in '${piece}'`);
56
57
  }
@@ -91,7 +92,7 @@ function parseFlare(source, filePath, defaultName) {
91
92
  name = value;
92
93
  continue;
93
94
  }
94
- const packet = line.match(new RegExp(`^packet\\s+(${IDENT})\\s*\\((.*)\\)\\s+from\\s+(Client|Server)(?:\\s+(unreliable|reliable))?\\s*$`, "i"));
95
+ const packet = line.match(new RegExp(`^packet\\s+(${exports.FLARE_IDENT})\\s*\\((.*)\\)\\s+from\\s+(Client|Server)(?:\\s+(unreliable|reliable))?\\s*$`, "i"));
95
96
  if (packet) {
96
97
  const reliable = String(packet[4] || "reliable").toLowerCase() !== "unreliable";
97
98
  if (packetId > 255) {
@@ -107,7 +108,7 @@ function parseFlare(source, filePath, defaultName) {
107
108
  });
108
109
  continue;
109
110
  }
110
- const query = line.match(new RegExp(`^query\\s+(${IDENT})\\s*\\((.*)\\)\\s*->\\s*(${IDENT})\\s*$`, "i"));
111
+ const query = line.match(new RegExp(`^query\\s+(${exports.FLARE_IDENT})\\s*\\((.*)\\)\\s*->\\s*(${exports.FLARE_IDENT})\\s*$`, "i"));
111
112
  if (query) {
112
113
  if (queryId > 255) {
113
114
  fail(file, lineNo, "too many queries (max 255)");
@@ -81,6 +81,7 @@ function writeVscode(root) {
81
81
  "*.server.clpp": "clpp",
82
82
  "*.client.clpp": "clpp",
83
83
  "*.plugin.clpp": "clpp",
84
+ "*.flare": "flare",
84
85
  };
85
86
  writeJson(settingsPath, {
86
87
  ...current,
@@ -90,7 +91,7 @@ function writeVscode(root) {
90
91
  const extensions = readJsonObject(extensionsPath);
91
92
  writeJson(extensionsPath, {
92
93
  ...extensions,
93
- recommendations: uniqueStrings(extensions.recommendations),
94
+ recommendations: uniqueStrings(extensions.recommendations, ["kartzdev.cluaupp-flare"]),
94
95
  unwantedRecommendations: uniqueStrings(extensions.unwantedRecommendations, ["ms-vscode.cpptools"]),
95
96
  });
96
97
  }
@@ -107,7 +108,7 @@ function installEditorExtension(cluauppRoot = package_info_js_1.projectRoot) {
107
108
  if (!node_fs_1.default.existsSync(from)) {
108
109
  return [];
109
110
  }
110
- const id = `kartzdev.cluaupp-diagnostics-${package_info_js_1.pkg.version}`;
111
+ const id = `kartzdev.cluaupp-flare-${package_info_js_1.pkg.version}`;
111
112
  const homes = [node_path_1.default.join(node_os_1.default.homedir(), ".cursor", "extensions"), node_path_1.default.join(node_os_1.default.homedir(), ".vscode", "extensions")];
112
113
  const installed = [];
113
114
  for (const home of homes) {
@@ -118,12 +119,29 @@ function installEditorExtension(cluauppRoot = package_info_js_1.projectRoot) {
118
119
  const dest = node_path_1.default.join(home, id);
119
120
  copyDir(from, dest);
120
121
  node_fs_1.default.writeFileSync(node_path_1.default.join(dest, "cluaupp.root"), node_path_1.default.resolve(cluauppRoot), "utf8");
122
+ const manifestPath = node_path_1.default.join(dest, "package.json");
123
+ if (node_fs_1.default.existsSync(manifestPath)) {
124
+ try {
125
+ const manifest = JSON.parse(node_fs_1.default.readFileSync(manifestPath, "utf8"));
126
+ manifest.version = package_info_js_1.pkg.version;
127
+ node_fs_1.default.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, "\t")}\n`, "utf8");
128
+ }
129
+ catch {
130
+ // keep the copied manifest
131
+ }
132
+ }
121
133
  installed.push(dest);
122
134
  }
123
135
  return installed;
124
136
  }
125
137
  async function installEditorSupport() {
126
138
  const local = installEditorExtension();
139
+ if (local.length > 0) {
140
+ console.log("cluaupp: Flare IntelliSense installed");
141
+ for (const dest of local) {
142
+ console.log(" ", dest);
143
+ }
144
+ }
127
145
  if ((0, runner_js_1.hasClpp)()) {
128
146
  const result = (0, node_child_process_1.spawnSync)("clpp", ["install"], { encoding: "utf8", windowsHide: true });
129
147
  if (result.status === 0) {