cluaupp 0.1.2 → 0.1.4
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 +20 -1
- package/README.md +5 -8
- package/docs/README.md +13 -10
- package/docs/architecture.md +18 -11
- package/docs/cli.md +2 -2
- package/docs/config.md +1 -1
- package/docs/cpp-advanced.md +10 -6
- package/docs/cpp-organization.md +3 -1
- 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 +12 -6
- package/docs/intellisense.md +31 -0
- package/docs/intro.md +2 -2
- 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 +49 -0
- package/docs/oop/index.md +22 -0
- package/docs/oop/modules.md +86 -0
- package/docs/oop/services.md +83 -0
- package/docs/print-cout.md +91 -0
- package/docs/syntax.md +59 -5
- 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 +108 -48
- package/src/cli.js +85 -10
- package/src/client/init.client.cpp +5 -0
- package/src/compile.js +20 -4
- package/src/editor-install.js +218 -0
- package/src/emit.js +320 -8
- package/src/intellisense.js +1368 -0
- package/src/layout.js +129 -0
- package/src/lex.js +17 -9
- package/src/libs.js +95 -16
- package/src/lsp.js +227 -0
- package/src/parse.js +187 -6
- package/src/preprocess.js +71 -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 +64 -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/compile_flags.txt +2 -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",
|
|
@@ -301,42 +303,118 @@ function requireCluauppLib(name) {
|
|
|
301
303
|
return `require(ReplicatedStorage.CluauppLibs.${name})`;
|
|
302
304
|
}
|
|
303
305
|
|
|
304
|
-
function
|
|
306
|
+
function emitRequireParts(libraries) {
|
|
307
|
+
const api = [];
|
|
308
|
+
const types = [];
|
|
305
309
|
if (!libraries.length) {
|
|
306
|
-
return "";
|
|
310
|
+
return { api: "", types: "" };
|
|
307
311
|
}
|
|
308
|
-
const
|
|
312
|
+
api.push('const ReplicatedStorage = game:GetService("ReplicatedStorage")');
|
|
309
313
|
for (const spec of libraries) {
|
|
310
|
-
|
|
314
|
+
api.push(`const ${spec.bind} = ${requireCluauppLib(spec.file)}`);
|
|
311
315
|
const exported = TYPE_EXPORTS[spec.bind];
|
|
312
316
|
if (exported) {
|
|
313
|
-
|
|
317
|
+
types.push(`type ${spec.bind} = ${spec.bind}.${exported}`);
|
|
314
318
|
}
|
|
315
319
|
const extra = EXTRA_TYPE_EXPORTS[spec.bind];
|
|
316
320
|
if (extra) {
|
|
317
321
|
for (const [alias, exportedName] of Object.entries(extra)) {
|
|
318
|
-
|
|
322
|
+
types.push(`type ${alias} = ${spec.bind}.${exportedName}`);
|
|
319
323
|
}
|
|
320
324
|
}
|
|
321
325
|
}
|
|
322
|
-
return
|
|
326
|
+
return {
|
|
327
|
+
api: `${api.join("\n")}\n`,
|
|
328
|
+
types: types.length ? `${types.join("\n")}\n` : "",
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function emitRequires(libraries) {
|
|
333
|
+
const { api, types } = emitRequireParts(libraries);
|
|
334
|
+
return [api, types].filter((part) => Boolean(part && part.trim())).join("\n");
|
|
323
335
|
}
|
|
324
336
|
|
|
325
337
|
function insertRequires(luau, libraries) {
|
|
326
|
-
const block = emitRequires(libraries);
|
|
338
|
+
const block = emitRequires(libraries).trimEnd();
|
|
327
339
|
if (!block) {
|
|
328
340
|
return luau;
|
|
329
341
|
}
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
if (index === -1) {
|
|
342
|
+
const match = String(luau).match(/^(?:--[^\n]*\n)+/);
|
|
343
|
+
if (!match) {
|
|
333
344
|
return `${block}\n${luau}`;
|
|
334
345
|
}
|
|
335
|
-
const insertAt =
|
|
336
|
-
|
|
337
|
-
|
|
346
|
+
const insertAt = match[0].length;
|
|
347
|
+
const rest = luau.slice(insertAt).replace(/^\n*/, "\n");
|
|
348
|
+
return `${luau.slice(0, insertAt)}\n${block}\n${rest}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function robloxRequireFrom(fromOutRel, toOutRel) {
|
|
352
|
+
const fromDir = path.posix.dirname(String(fromOutRel || "module.luau").replace(/\\/g, "/"));
|
|
353
|
+
const toMod = String(toOutRel || "module.luau")
|
|
354
|
+
.replace(/\\/g, "/")
|
|
355
|
+
.replace(/\.luau$/i, "");
|
|
356
|
+
let rel = path.posix.relative(fromDir, toMod);
|
|
357
|
+
if (!rel || rel === ".") {
|
|
358
|
+
rel = path.posix.basename(toMod);
|
|
359
|
+
}
|
|
360
|
+
const parts = rel.split("/");
|
|
361
|
+
let expr = "script.Parent";
|
|
362
|
+
for (const part of parts) {
|
|
363
|
+
if (part === "..") {
|
|
364
|
+
expr += ".Parent";
|
|
365
|
+
} else if (part && part !== ".") {
|
|
366
|
+
expr += `.${part}`;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return `require(${expr})`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function moduleIsUsed(luau, spec) {
|
|
373
|
+
if (String(luau).includes(spec.name)) {
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
const exported = [...((spec.exports && spec.exports.consts) || []), ...((spec.exports && spec.exports.structs) || [])];
|
|
377
|
+
return exported.some((name) => String(luau).includes(name));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function insertModuleRequires(luau, modules, fromOutRel) {
|
|
381
|
+
if (!modules || modules.length === 0) {
|
|
382
|
+
return luau;
|
|
383
|
+
}
|
|
384
|
+
const lines = [];
|
|
385
|
+
const seen = new Set();
|
|
386
|
+
for (const spec of modules) {
|
|
387
|
+
if (!spec || !spec.name || seen.has(spec.name)) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (fromOutRel && !moduleIsUsed(luau, spec)) {
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
seen.add(spec.name);
|
|
394
|
+
lines.push(`const ${spec.name} = ${robloxRequireFrom(fromOutRel, spec.outRel)}`);
|
|
395
|
+
for (const name of (spec.exports && spec.exports.consts) || []) {
|
|
396
|
+
if (name !== spec.name) {
|
|
397
|
+
lines.push(`const ${name} = ${spec.name}.${name}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if ((spec.exports && spec.exports.structs || []).includes(spec.name) && !spec.impl) {
|
|
401
|
+
lines.push(`type ${spec.name} = typeof(${spec.name}())`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (lines.length === 0) {
|
|
405
|
+
return luau;
|
|
406
|
+
}
|
|
407
|
+
return insertBlock(luau, lines.join("\n"));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function insertBlock(luau, block) {
|
|
411
|
+
const match = String(luau).match(/^(?:--[^\n]*\n)+/);
|
|
412
|
+
if (!match) {
|
|
413
|
+
return `${block}\n${luau}`;
|
|
338
414
|
}
|
|
339
|
-
|
|
415
|
+
const insertAt = match[0].length;
|
|
416
|
+
const rest = luau.slice(insertAt).replace(/^\n*/, "\n");
|
|
417
|
+
return `${luau.slice(0, insertAt)}\n${block}\n${rest}`;
|
|
340
418
|
}
|
|
341
419
|
|
|
342
420
|
module.exports = {
|
|
@@ -352,4 +430,5 @@ module.exports = {
|
|
|
352
430
|
requireCluauppLib,
|
|
353
431
|
emitRequires,
|
|
354
432
|
insertRequires,
|
|
433
|
+
insertModuleRequires,
|
|
355
434
|
};
|
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 };
|