cluaupp 0.1.2 → 0.1.5
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/CHANGELOG.md +29 -1
- package/README.md +10 -13
- package/docs/README.md +13 -10
- package/docs/architecture.md +30 -73
- package/docs/cli.md +5 -6
- package/docs/comparison.md +3 -3
- package/docs/config.md +6 -6
- package/docs/cpp-advanced.md +10 -6
- package/docs/cpp-organization.md +4 -2
- package/docs/cpp-safety.md +11 -7
- package/docs/cpp-types.md +1 -1
- package/docs/examples/_category_.json +5 -0
- package/docs/examples/combat.md +239 -0
- package/docs/examples/data-boot.md +98 -0
- package/docs/examples/hud.md +96 -0
- package/docs/examples/index.md +43 -0
- package/docs/examples/leaderstats.md +140 -0
- package/docs/examples/shop.md +122 -0
- package/docs/examples/sword.md +108 -0
- package/docs/getting-started.md +14 -7
- package/docs/intellisense.md +31 -0
- package/docs/intro.md +3 -3
- package/docs/libraries/_category_.json +5 -0
- package/docs/libraries/dataservice.md +104 -0
- package/docs/libraries/index.md +22 -0
- package/docs/libraries/janitor.md +65 -0
- package/docs/libraries/more.md +79 -0
- package/docs/libraries/net.md +35 -0
- package/docs/libraries/promise.md +27 -0
- package/docs/oop/_category_.json +5 -0
- package/docs/oop/file-tags.md +48 -0
- package/docs/oop/index.md +22 -0
- package/docs/oop/modules.md +88 -0
- package/docs/oop/services.md +76 -0
- package/docs/print-cout.md +91 -0
- package/docs/syntax.md +63 -8
- package/editors/vscode/extension.js +146 -0
- package/editors/vscode/package.json +25 -0
- package/include/cluaupp/libs/janitor.hpp +7 -3
- package/include/cluaupp/roblox.hpp +19 -0
- package/package.json +66 -65
- package/runtime/Janitor/init.luau +4 -34
- package/src/architecture.js +174 -66
- package/src/cli.js +88 -21
- package/src/client/init.client.cpp +5 -0
- package/src/compile.js +77 -7
- package/src/editor-install.js +218 -0
- package/src/emit.js +321 -9
- package/src/headers.js +234 -0
- package/src/intellisense.js +1368 -0
- package/src/layout.js +129 -0
- package/src/lex.js +17 -9
- package/src/libs.js +165 -18
- package/src/lsp.js +227 -0
- package/src/parse.js +187 -6
- package/src/preprocess.js +118 -3
- package/src/server/leaderstats.server.cpp +28 -0
- package/src/shared/config.cpp +5 -0
- package/src/shared/config.h +3 -0
- package/src/understand.js +66 -6
- package/templates/game/.clangd +11 -0
- package/templates/game/.vscode/c_cpp_properties.json +6 -2
- package/templates/game/.vscode/extensions.json +6 -0
- package/templates/game/.vscode/settings.json +16 -2
- package/templates/game/cluaupp.config.json +2 -2
- package/templates/game/compile_flags.txt +2 -0
- package/templates/game/src/client/init.client.cpp +1 -0
- package/templates/game/src/server/leaderstats.server.cpp +1 -0
- package/docs/libraries.md +0 -80
package/src/layout.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const SECTION = {
|
|
4
|
+
api: "API",
|
|
5
|
+
constants: "CONSTANTES",
|
|
6
|
+
variables: "VARIAVEIS",
|
|
7
|
+
types: "TIPAGENS",
|
|
8
|
+
support: "FUNÇÕES DE SUPORTE",
|
|
9
|
+
principal: "FUNÇÕES PRINCIPAIS",
|
|
10
|
+
cleanup: "LIMPEZA",
|
|
11
|
+
returns: "RETORNO",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const CLEANUP_FN = /^(OnClose|OnPlayerRemoving|Cleanup|Shutdown|OnDestroy)$/i;
|
|
15
|
+
const PRINCIPAL_FN = /^(init|start|main|setup)/i;
|
|
16
|
+
const ENTRY_FN = /^(init|main)$/i;
|
|
17
|
+
|
|
18
|
+
function isGetService(node) {
|
|
19
|
+
return Boolean(node && node.type === "getService");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isDataServiceAlias(node) {
|
|
23
|
+
let current = node;
|
|
24
|
+
while (current && current.type === "member") {
|
|
25
|
+
if (current.object && current.object.type === "ident" && current.object.name === "DataService") {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
current = current.object;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isJanitorDecl(decl) {
|
|
34
|
+
if (!decl || decl.type !== "decl") {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const name = String(decl.name || "").toLowerCase();
|
|
38
|
+
const typeName = String(decl.valueType || "").toLowerCase();
|
|
39
|
+
const created = decl.value && decl.value.type === "new" && decl.value.className === "Janitor";
|
|
40
|
+
return name.includes("janitor") || typeName.includes("janitor") || created;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isApiDecl(decl) {
|
|
44
|
+
if (!decl || decl.type !== "decl") {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return isGetService(decl.value) || isDataServiceAlias(decl.value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function classifyDecl(decl) {
|
|
51
|
+
if (!decl || decl.type === "proto") {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if (decl.type === "decl") {
|
|
55
|
+
if (isApiDecl(decl)) {
|
|
56
|
+
return "api";
|
|
57
|
+
}
|
|
58
|
+
if (isJanitorDecl(decl)) {
|
|
59
|
+
return "variables";
|
|
60
|
+
}
|
|
61
|
+
if (decl.isConst) {
|
|
62
|
+
return "constants";
|
|
63
|
+
}
|
|
64
|
+
return "variables";
|
|
65
|
+
}
|
|
66
|
+
if (decl.type === "function") {
|
|
67
|
+
if (CLEANUP_FN.test(decl.name || "")) {
|
|
68
|
+
return "cleanup";
|
|
69
|
+
}
|
|
70
|
+
if (PRINCIPAL_FN.test(decl.name || "")) {
|
|
71
|
+
return "principal";
|
|
72
|
+
}
|
|
73
|
+
return "support";
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function organizeDecls(keep) {
|
|
79
|
+
const groups = {
|
|
80
|
+
api: [],
|
|
81
|
+
constants: [],
|
|
82
|
+
variables: [],
|
|
83
|
+
support: [],
|
|
84
|
+
principal: [],
|
|
85
|
+
cleanup: [],
|
|
86
|
+
};
|
|
87
|
+
for (const decl of keep) {
|
|
88
|
+
const bucket = classifyDecl(decl);
|
|
89
|
+
if (bucket && groups[bucket]) {
|
|
90
|
+
groups[bucket].push(decl);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return groups;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function janitorNames(keep) {
|
|
97
|
+
return (keep || []).filter(isJanitorDecl).map((decl) => decl.name);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function emitSection(_title, body) {
|
|
101
|
+
const text = String(body || "").trimEnd();
|
|
102
|
+
if (!text) {
|
|
103
|
+
return "";
|
|
104
|
+
}
|
|
105
|
+
return `${text}\n\n`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function joinBlocks(parts) {
|
|
109
|
+
return parts.filter((part) => Boolean(part && String(part).trim())).join("\n\n");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function commentLine(title) {
|
|
113
|
+
return `-- ${title}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = {
|
|
117
|
+
SECTION,
|
|
118
|
+
CLEANUP_FN,
|
|
119
|
+
PRINCIPAL_FN,
|
|
120
|
+
ENTRY_FN,
|
|
121
|
+
isJanitorDecl,
|
|
122
|
+
isApiDecl,
|
|
123
|
+
classifyDecl,
|
|
124
|
+
organizeDecls,
|
|
125
|
+
janitorNames,
|
|
126
|
+
emitSection,
|
|
127
|
+
joinBlocks,
|
|
128
|
+
commentLine,
|
|
129
|
+
};
|
package/src/lex.js
CHANGED
|
@@ -28,6 +28,10 @@ const KEYWORDS = new Set([
|
|
|
28
28
|
"typedef",
|
|
29
29
|
"extern",
|
|
30
30
|
"enum",
|
|
31
|
+
"switch",
|
|
32
|
+
"case",
|
|
33
|
+
"default",
|
|
34
|
+
"break",
|
|
31
35
|
]);
|
|
32
36
|
|
|
33
37
|
function tokenize(source) {
|
|
@@ -36,8 +40,8 @@ function tokenize(source) {
|
|
|
36
40
|
let line = 1;
|
|
37
41
|
let col = 1;
|
|
38
42
|
|
|
39
|
-
const push = (type, value, startLine, startCol) => {
|
|
40
|
-
tokens.push({ type, value, line: startLine, col: startCol });
|
|
43
|
+
const push = (type, value, startLine, startCol, start, end) => {
|
|
44
|
+
tokens.push({ type, value, line: startLine, col: startCol, start, end });
|
|
41
45
|
};
|
|
42
46
|
|
|
43
47
|
while (i < source.length) {
|
|
@@ -87,6 +91,7 @@ function tokenize(source) {
|
|
|
87
91
|
const startCol = col;
|
|
88
92
|
|
|
89
93
|
if (c === '"') {
|
|
94
|
+
const start = i;
|
|
90
95
|
i += 1;
|
|
91
96
|
col += 1;
|
|
92
97
|
let value = "";
|
|
@@ -103,46 +108,49 @@ function tokenize(source) {
|
|
|
103
108
|
}
|
|
104
109
|
i += 1;
|
|
105
110
|
col += 1;
|
|
106
|
-
push("string", value, startLine, startCol);
|
|
111
|
+
push("string", value, startLine, startCol, start, i);
|
|
107
112
|
continue;
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
if (/[0-9]/.test(c)) {
|
|
116
|
+
const start = i;
|
|
111
117
|
let value = "";
|
|
112
118
|
while (i < source.length && /[0-9.]/.test(source[i])) {
|
|
113
119
|
value += source[i];
|
|
114
120
|
i += 1;
|
|
115
121
|
col += 1;
|
|
116
122
|
}
|
|
117
|
-
push("number", value, startLine, startCol);
|
|
123
|
+
push("number", value, startLine, startCol, start, i);
|
|
118
124
|
continue;
|
|
119
125
|
}
|
|
120
126
|
|
|
121
127
|
if (/[A-Za-z_]/.test(c)) {
|
|
128
|
+
const start = i;
|
|
122
129
|
let value = "";
|
|
123
130
|
while (i < source.length && /[A-Za-z0-9_]/.test(source[i])) {
|
|
124
131
|
value += source[i];
|
|
125
132
|
i += 1;
|
|
126
133
|
col += 1;
|
|
127
134
|
}
|
|
128
|
-
push(KEYWORDS.has(value) ? "kw" : "ident", value, startLine, startCol);
|
|
135
|
+
push(KEYWORDS.has(value) ? "kw" : "ident", value, startLine, startCol, start, i);
|
|
129
136
|
continue;
|
|
130
137
|
}
|
|
131
138
|
|
|
132
139
|
const two = source.slice(i, i + 2);
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
const start = i;
|
|
141
|
+
if (["->", "==", "!=", "<=", ">=", "&&", "||", "::", "<<", ">>"].includes(two)) {
|
|
142
|
+
push("op", two, startLine, startCol, start, i + 2);
|
|
135
143
|
i += 2;
|
|
136
144
|
col += 2;
|
|
137
145
|
continue;
|
|
138
146
|
}
|
|
139
147
|
|
|
140
|
-
push("op", c, startLine, startCol);
|
|
148
|
+
push("op", c, startLine, startCol, start, i + 1);
|
|
141
149
|
i += 1;
|
|
142
150
|
col += 1;
|
|
143
151
|
}
|
|
144
152
|
|
|
145
|
-
push("eof", "", line, col);
|
|
153
|
+
push("eof", "", line, col, source.length, source.length);
|
|
146
154
|
return tokens;
|
|
147
155
|
}
|
|
148
156
|
|
package/src/libs.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
+
const path = require("path");
|
|
4
|
+
|
|
3
5
|
const TYPE_EXPORTS = {
|
|
4
6
|
Janitor: "Janitor",
|
|
5
7
|
Promise: "Promise",
|
|
@@ -100,12 +102,12 @@ const LIBRARY_METHODS = new Set([
|
|
|
100
102
|
"Remove",
|
|
101
103
|
"RemoveNoClean",
|
|
102
104
|
"RemoveList",
|
|
105
|
+
"RemoveListNoClean",
|
|
103
106
|
"GetAll",
|
|
104
107
|
"Cleanup",
|
|
105
108
|
"Destroy",
|
|
106
109
|
"LinkToInstance",
|
|
107
110
|
"LinkToInstances",
|
|
108
|
-
"Has",
|
|
109
111
|
"Then",
|
|
110
112
|
"Catch",
|
|
111
113
|
"Finally",
|
|
@@ -297,46 +299,189 @@ function collectLibraries(source, ast) {
|
|
|
297
299
|
return unique;
|
|
298
300
|
}
|
|
299
301
|
|
|
302
|
+
const ROJO_ROOTS = {
|
|
303
|
+
shared: { service: "ReplicatedStorage", expr: "ReplicatedStorage.Cluaupp" },
|
|
304
|
+
server: { service: "ServerScriptService", expr: "ServerScriptService.Cluaupp" },
|
|
305
|
+
client: { service: "StarterPlayer", expr: "StarterPlayer.StarterPlayerScripts.Cluaupp" },
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const SERVICE_ORDER = ["ReplicatedStorage", "ServerScriptService", "StarterPlayer"];
|
|
309
|
+
|
|
310
|
+
const SERVICE_GET = {
|
|
311
|
+
ReplicatedStorage: 'const ReplicatedStorage = game:GetService("ReplicatedStorage")',
|
|
312
|
+
ServerScriptService: 'const ServerScriptService = game:GetService("ServerScriptService")',
|
|
313
|
+
StarterPlayer: 'const StarterPlayer = game:GetService("StarterPlayer")',
|
|
314
|
+
};
|
|
315
|
+
|
|
300
316
|
function requireCluauppLib(name) {
|
|
301
317
|
return `require(ReplicatedStorage.CluauppLibs.${name})`;
|
|
302
318
|
}
|
|
303
319
|
|
|
304
|
-
function
|
|
305
|
-
|
|
306
|
-
|
|
320
|
+
function parseOutRel(toOutRel) {
|
|
321
|
+
const posix = String(toOutRel || "module.luau")
|
|
322
|
+
.replace(/\\/g, "/")
|
|
323
|
+
.replace(/\.luau$/i, "");
|
|
324
|
+
const parts = posix.split("/").filter(Boolean);
|
|
325
|
+
const tree = parts[0];
|
|
326
|
+
const root = ROJO_ROOTS[tree] || null;
|
|
327
|
+
const rest = root ? parts.slice(1) : parts;
|
|
328
|
+
return { tree, root, rest, posix };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function robloxRequireFrom(_fromOutRel, toOutRel) {
|
|
332
|
+
const { root, rest } = parseOutRel(toOutRel);
|
|
333
|
+
if (root) {
|
|
334
|
+
const tail = rest.join(".");
|
|
335
|
+
return tail ? `require(${root.expr}.${tail})` : `require(${root.expr})`;
|
|
336
|
+
}
|
|
337
|
+
const fromDir = path.posix.dirname(String(_fromOutRel || "module.luau").replace(/\\/g, "/"));
|
|
338
|
+
const toMod = String(toOutRel || "module.luau")
|
|
339
|
+
.replace(/\\/g, "/")
|
|
340
|
+
.replace(/\.luau$/i, "");
|
|
341
|
+
let rel = path.posix.relative(fromDir, toMod);
|
|
342
|
+
if (!rel || rel === ".") {
|
|
343
|
+
rel = path.posix.basename(toMod);
|
|
307
344
|
}
|
|
308
|
-
const
|
|
345
|
+
const parts = rel.split("/");
|
|
346
|
+
let expr = "script.Parent";
|
|
347
|
+
for (const part of parts) {
|
|
348
|
+
if (part === "..") {
|
|
349
|
+
expr += ".Parent";
|
|
350
|
+
} else if (part && part !== ".") {
|
|
351
|
+
expr += `.${part}`;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return `require(${expr})`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function emitLibraryLines(libraries) {
|
|
358
|
+
const api = [];
|
|
359
|
+
const types = [];
|
|
309
360
|
for (const spec of libraries) {
|
|
310
|
-
|
|
361
|
+
api.push(`const ${spec.bind} = ${requireCluauppLib(spec.file)}`);
|
|
311
362
|
const exported = TYPE_EXPORTS[spec.bind];
|
|
312
363
|
if (exported) {
|
|
313
|
-
|
|
364
|
+
types.push(`type ${spec.bind} = ${spec.bind}.${exported}`);
|
|
314
365
|
}
|
|
315
366
|
const extra = EXTRA_TYPE_EXPORTS[spec.bind];
|
|
316
367
|
if (extra) {
|
|
317
368
|
for (const [alias, exportedName] of Object.entries(extra)) {
|
|
318
|
-
|
|
369
|
+
types.push(`type ${alias} = ${spec.bind}.${exportedName}`);
|
|
319
370
|
}
|
|
320
371
|
}
|
|
321
372
|
}
|
|
322
|
-
return
|
|
373
|
+
return { api, types };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function emitRequireParts(libraries) {
|
|
377
|
+
if (!libraries.length) {
|
|
378
|
+
return { api: "", types: "" };
|
|
379
|
+
}
|
|
380
|
+
const { api, types } = emitLibraryLines(libraries);
|
|
381
|
+
api.unshift(SERVICE_GET.ReplicatedStorage);
|
|
382
|
+
return {
|
|
383
|
+
api: `${api.join("\n")}\n`,
|
|
384
|
+
types: types.length ? `${types.join("\n")}\n` : "",
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function emitRequires(libraries) {
|
|
389
|
+
const { api, types } = emitRequireParts(libraries);
|
|
390
|
+
return [api, types].filter((part) => Boolean(part && part.trim())).join("\n");
|
|
323
391
|
}
|
|
324
392
|
|
|
325
393
|
function insertRequires(luau, libraries) {
|
|
326
|
-
const block = emitRequires(libraries);
|
|
394
|
+
const block = emitRequires(libraries).trimEnd();
|
|
327
395
|
if (!block) {
|
|
328
396
|
return luau;
|
|
329
397
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
398
|
+
return insertBlock(luau, block);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function moduleIsUsed(luau, spec) {
|
|
402
|
+
if (String(luau).includes(spec.name)) {
|
|
403
|
+
return true;
|
|
334
404
|
}
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
405
|
+
const exported = [...((spec.exports && spec.exports.consts) || []), ...((spec.exports && spec.exports.structs) || [])];
|
|
406
|
+
return exported.some((name) => String(luau).includes(name));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function moduleRequireLines(modules, fromOutRel, luau) {
|
|
410
|
+
const lines = [];
|
|
411
|
+
const services = new Set();
|
|
412
|
+
const seen = new Set();
|
|
413
|
+
for (const spec of modules || []) {
|
|
414
|
+
if (!spec || !spec.name || seen.has(spec.name)) {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (fromOutRel && !moduleIsUsed(luau, spec)) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
seen.add(spec.name);
|
|
421
|
+
const { root } = parseOutRel(spec.outRel);
|
|
422
|
+
if (root) {
|
|
423
|
+
services.add(root.service);
|
|
424
|
+
}
|
|
425
|
+
lines.push(`const ${spec.name} = ${robloxRequireFrom(fromOutRel, spec.outRel)}`);
|
|
426
|
+
for (const name of (spec.exports && spec.exports.consts) || []) {
|
|
427
|
+
if (name !== spec.name) {
|
|
428
|
+
lines.push(`const ${name} = ${spec.name}.${name}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (((spec.exports && spec.exports.structs) || []).includes(spec.name) || (spec.exports && spec.exports.hasProtos)) {
|
|
432
|
+
lines.push(`type ${spec.name} = ${spec.name}.${spec.name}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return { lines, services };
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function insertModuleRequires(luau, modules, fromOutRel) {
|
|
439
|
+
return insertPreamble(luau, modules, [], fromOutRel);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function stripServiceGets(luau, services) {
|
|
443
|
+
let next = String(luau);
|
|
444
|
+
for (const name of services) {
|
|
445
|
+
const line = SERVICE_GET[name];
|
|
446
|
+
if (!line) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
next = next.replace(new RegExp(`^${line.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\n?`, "m"), "");
|
|
450
|
+
}
|
|
451
|
+
return next;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function insertPreamble(luau, modules, libraries, fromOutRel) {
|
|
455
|
+
const { lines: moduleLines, services } = moduleRequireLines(modules, fromOutRel, luau);
|
|
456
|
+
const libList = libraries || [];
|
|
457
|
+
if (libList.length) {
|
|
458
|
+
services.add("ReplicatedStorage");
|
|
459
|
+
}
|
|
460
|
+
if (moduleLines.length === 0 && libList.length === 0) {
|
|
461
|
+
return luau;
|
|
462
|
+
}
|
|
463
|
+
const { api: libApi, types: libTypes } = emitLibraryLines(libList);
|
|
464
|
+
const blockLines = [];
|
|
465
|
+
for (const name of SERVICE_ORDER) {
|
|
466
|
+
if (services.has(name)) {
|
|
467
|
+
blockLines.push(SERVICE_GET[name]);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
blockLines.push(...moduleLines);
|
|
471
|
+
blockLines.push(...libApi);
|
|
472
|
+
blockLines.push(...libTypes);
|
|
473
|
+
const body = stripServiceGets(luau, services);
|
|
474
|
+
return insertBlock(body, blockLines.join("\n"));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function insertBlock(luau, block) {
|
|
478
|
+
const match = String(luau).match(/^(?:--[^\n]*\n)+/);
|
|
479
|
+
if (!match) {
|
|
480
|
+
return `${block}\n${luau}`;
|
|
338
481
|
}
|
|
339
|
-
|
|
482
|
+
const insertAt = match[0].length;
|
|
483
|
+
const rest = luau.slice(insertAt).replace(/^\n*/, "\n");
|
|
484
|
+
return `${luau.slice(0, insertAt)}\n${block}\n${rest}`;
|
|
340
485
|
}
|
|
341
486
|
|
|
342
487
|
module.exports = {
|
|
@@ -352,4 +497,6 @@ module.exports = {
|
|
|
352
497
|
requireCluauppLib,
|
|
353
498
|
emitRequires,
|
|
354
499
|
insertRequires,
|
|
500
|
+
insertModuleRequires,
|
|
501
|
+
insertPreamble,
|
|
355
502
|
};
|
package/src/lsp.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { completeAt, hoverAt, definitionAt, diagnosticsFor } = require("./intellisense");
|
|
4
|
+
|
|
5
|
+
function readMessage(buffer) {
|
|
6
|
+
const headerEnd = buffer.indexOf("\r\n\r\n");
|
|
7
|
+
if (headerEnd < 0) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const header = buffer.slice(0, headerEnd).toString("utf8");
|
|
11
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
12
|
+
if (!match) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const length = Number(match[1]);
|
|
16
|
+
const start = headerEnd + 4;
|
|
17
|
+
if (buffer.length < start + length) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const json = buffer.slice(start, start + length).toString("utf8");
|
|
21
|
+
return { message: JSON.parse(json), rest: buffer.slice(start + length) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function send(message) {
|
|
25
|
+
const json = JSON.stringify(message);
|
|
26
|
+
const payload = Buffer.from(json, "utf8");
|
|
27
|
+
process.stdout.write(`Content-Length: ${payload.length}\r\n\r\n`);
|
|
28
|
+
process.stdout.write(payload);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function posToOffset(text, position) {
|
|
32
|
+
const lines = text.split(/\n/);
|
|
33
|
+
let offset = 0;
|
|
34
|
+
for (let i = 0; i < position.line; i += 1) {
|
|
35
|
+
offset += (lines[i] || "").length + 1;
|
|
36
|
+
}
|
|
37
|
+
return offset + (position.character || 0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function kindNumber(kind) {
|
|
41
|
+
switch (kind) {
|
|
42
|
+
case "method":
|
|
43
|
+
case "function":
|
|
44
|
+
return 3;
|
|
45
|
+
case "constructor":
|
|
46
|
+
return 4;
|
|
47
|
+
case "property":
|
|
48
|
+
case "variable":
|
|
49
|
+
return 6;
|
|
50
|
+
case "class":
|
|
51
|
+
return 7;
|
|
52
|
+
case "enum":
|
|
53
|
+
return 13;
|
|
54
|
+
case "keyword":
|
|
55
|
+
return 14;
|
|
56
|
+
case "event":
|
|
57
|
+
return 23;
|
|
58
|
+
case "file":
|
|
59
|
+
return 17;
|
|
60
|
+
default:
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function start(options = {}) {
|
|
66
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
67
|
+
const documents = new Map();
|
|
68
|
+
let buffer = Buffer.alloc(0);
|
|
69
|
+
let id = 0;
|
|
70
|
+
|
|
71
|
+
const uriPath = (uri) => {
|
|
72
|
+
let file = String(uri || "").replace(/^file:\/\//, "");
|
|
73
|
+
if (/^\/[A-Za-z]:/.test(file)) {
|
|
74
|
+
file = file.slice(1);
|
|
75
|
+
}
|
|
76
|
+
return decodeURIComponent(file);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const docText = (uri) => documents.get(uri) || "";
|
|
80
|
+
|
|
81
|
+
const publishDiagnostics = (uri) => {
|
|
82
|
+
const file = uriPath(uri);
|
|
83
|
+
const text = docText(uri);
|
|
84
|
+
const items = diagnosticsFor(text, file, {
|
|
85
|
+
filePath: file,
|
|
86
|
+
projectRoot,
|
|
87
|
+
includeDirs: [pathDirname(file), require("path").join(projectRoot, "src"), require("path").join(projectRoot, "include")],
|
|
88
|
+
});
|
|
89
|
+
send({
|
|
90
|
+
jsonrpc: "2.0",
|
|
91
|
+
method: "textDocument/publishDiagnostics",
|
|
92
|
+
params: {
|
|
93
|
+
uri,
|
|
94
|
+
diagnostics: items.map((item) => ({
|
|
95
|
+
range: {
|
|
96
|
+
start: { line: Math.max(0, (item.line || 1) - 1), character: Math.max(0, (item.col || 1) - 1) },
|
|
97
|
+
end: { line: Math.max(0, (item.line || 1) - 1), character: Math.max(1, item.col || 1) },
|
|
98
|
+
},
|
|
99
|
+
severity: 1,
|
|
100
|
+
source: "cluaupp",
|
|
101
|
+
message: item.message,
|
|
102
|
+
})),
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const pathDirname = (file) => require("path").dirname(file);
|
|
108
|
+
|
|
109
|
+
const handle = (message) => {
|
|
110
|
+
const { method, params, id: reqId } = message;
|
|
111
|
+
if (method === "initialize") {
|
|
112
|
+
send({
|
|
113
|
+
jsonrpc: "2.0",
|
|
114
|
+
id: reqId,
|
|
115
|
+
result: {
|
|
116
|
+
capabilities: {
|
|
117
|
+
textDocumentSync: 1,
|
|
118
|
+
completionProvider: { triggerCharacters: [".", ":", ">", "<", '"', "/"] },
|
|
119
|
+
hoverProvider: true,
|
|
120
|
+
definitionProvider: true,
|
|
121
|
+
},
|
|
122
|
+
serverInfo: { name: "cluaupp", version: require("../package.json").version },
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (method === "initialized" || method === "shutdown") {
|
|
128
|
+
if (reqId !== undefined) {
|
|
129
|
+
send({ jsonrpc: "2.0", id: reqId, result: null });
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (method === "exit") {
|
|
134
|
+
process.exit(0);
|
|
135
|
+
}
|
|
136
|
+
if (method === "textDocument/didOpen") {
|
|
137
|
+
documents.set(params.textDocument.uri, params.textDocument.text);
|
|
138
|
+
publishDiagnostics(params.textDocument.uri);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (method === "textDocument/didChange") {
|
|
142
|
+
const last = params.contentChanges[params.contentChanges.length - 1];
|
|
143
|
+
if (last && last.text !== undefined && !last.range) {
|
|
144
|
+
documents.set(params.textDocument.uri, last.text);
|
|
145
|
+
}
|
|
146
|
+
publishDiagnostics(params.textDocument.uri);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (method === "textDocument/didClose") {
|
|
150
|
+
documents.delete(params.textDocument.uri);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (method === "textDocument/completion") {
|
|
154
|
+
const uri = params.textDocument.uri;
|
|
155
|
+
const text = docText(uri);
|
|
156
|
+
const offset = posToOffset(text, params.position);
|
|
157
|
+
const items = completeAt(text, offset, { projectRoot, file: uriPath(uri) });
|
|
158
|
+
send({
|
|
159
|
+
jsonrpc: "2.0",
|
|
160
|
+
id: reqId,
|
|
161
|
+
result: {
|
|
162
|
+
isIncomplete: false,
|
|
163
|
+
items: items.map((item) => ({
|
|
164
|
+
label: item.name,
|
|
165
|
+
kind: kindNumber(item.kind),
|
|
166
|
+
detail: item.detail,
|
|
167
|
+
sortText: `${item.kind === "keyword" ? "2" : "0"}_${item.name}`,
|
|
168
|
+
insertText: item.name,
|
|
169
|
+
})),
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (method === "textDocument/hover") {
|
|
175
|
+
const uri = params.textDocument.uri;
|
|
176
|
+
const text = docText(uri);
|
|
177
|
+
const offset = posToOffset(text, params.position);
|
|
178
|
+
const hover = hoverAt(text, offset, { projectRoot, file: uriPath(uri) });
|
|
179
|
+
send({
|
|
180
|
+
jsonrpc: "2.0",
|
|
181
|
+
id: reqId,
|
|
182
|
+
result: hover
|
|
183
|
+
? { contents: { kind: "markdown", value: `**${hover.name}**\n\n\`${hover.detail || hover.type || ""}\`` } }
|
|
184
|
+
: null,
|
|
185
|
+
});
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (method === "textDocument/definition") {
|
|
189
|
+
const uri = params.textDocument.uri;
|
|
190
|
+
const text = docText(uri);
|
|
191
|
+
const offset = posToOffset(text, params.position);
|
|
192
|
+
const def = definitionAt(text, offset, { projectRoot, file: uriPath(uri) });
|
|
193
|
+
if (!def) {
|
|
194
|
+
send({ jsonrpc: "2.0", id: reqId, result: null });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
send({
|
|
198
|
+
jsonrpc: "2.0",
|
|
199
|
+
id: reqId,
|
|
200
|
+
result: {
|
|
201
|
+
uri: "file:///" + String(def.file).replace(/\\/g, "/"),
|
|
202
|
+
range: {
|
|
203
|
+
start: { line: Math.max(0, (def.line || 1) - 1), character: 0 },
|
|
204
|
+
end: { line: Math.max(0, (def.line || 1) - 1), character: 0 },
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (reqId !== undefined) {
|
|
211
|
+
send({ jsonrpc: "2.0", id: reqId, result: null });
|
|
212
|
+
}
|
|
213
|
+
id += 1;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
process.stdin.on("data", (chunk) => {
|
|
217
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
218
|
+
let parsed = readMessage(buffer);
|
|
219
|
+
while (parsed) {
|
|
220
|
+
handle(parsed.message);
|
|
221
|
+
buffer = parsed.rest;
|
|
222
|
+
parsed = readMessage(buffer);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = { start };
|