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
|
@@ -0,0 +1,1368 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const os = require("os");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const { tokenize } = require("./lex");
|
|
7
|
+
const { INSTANCE_TYPES, SERVICES } = require("./api");
|
|
8
|
+
|
|
9
|
+
const PACKAGE_ROOT = path.join(__dirname, "..");
|
|
10
|
+
const pkg = require("../package.json");
|
|
11
|
+
|
|
12
|
+
const KEYWORDS = [
|
|
13
|
+
"if",
|
|
14
|
+
"else",
|
|
15
|
+
"for",
|
|
16
|
+
"while",
|
|
17
|
+
"return",
|
|
18
|
+
"new",
|
|
19
|
+
"auto",
|
|
20
|
+
"const",
|
|
21
|
+
"void",
|
|
22
|
+
"int",
|
|
23
|
+
"bool",
|
|
24
|
+
"float",
|
|
25
|
+
"double",
|
|
26
|
+
"true",
|
|
27
|
+
"false",
|
|
28
|
+
"nullptr",
|
|
29
|
+
"struct",
|
|
30
|
+
"class",
|
|
31
|
+
"switch",
|
|
32
|
+
"case",
|
|
33
|
+
"default",
|
|
34
|
+
"break",
|
|
35
|
+
"template",
|
|
36
|
+
"namespace",
|
|
37
|
+
"public",
|
|
38
|
+
"private",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const GLOBALS = [
|
|
42
|
+
{ name: "game", type: "DataModel", kind: "variable", detail: "DataModel* game" },
|
|
43
|
+
{ name: "workspace", type: "Workspace", kind: "variable", detail: "Workspace* workspace" },
|
|
44
|
+
{ name: "script", type: "LuaSourceContainer", kind: "variable", detail: "LuaSourceContainer* script" },
|
|
45
|
+
{ name: "print", type: "void", kind: "function", detail: "void print(string message)" },
|
|
46
|
+
{ name: "warn", type: "void", kind: "function", detail: "void warn(string message)" },
|
|
47
|
+
{ name: "error", type: "void", kind: "function", detail: "void error(string message)" },
|
|
48
|
+
{ name: "GetService", type: "Instance", kind: "function", detail: "T* GetService<T>()" },
|
|
49
|
+
{ name: "cout", type: "cout", kind: "variable", detail: "cout" },
|
|
50
|
+
{ name: "cerr", type: "cerr", kind: "variable", detail: "cerr" },
|
|
51
|
+
{ name: "endl", type: "int", kind: "variable", detail: "const int endl" },
|
|
52
|
+
{ name: "tick", type: "double", kind: "function", detail: "double tick()" },
|
|
53
|
+
{ name: "time", type: "double", kind: "function", detail: "double time()" },
|
|
54
|
+
{ name: "wait", type: "void", kind: "function", detail: "void wait(double seconds = 0)" },
|
|
55
|
+
{ name: "spawn", type: "void", kind: "function", detail: "void spawn(void (*callback)())" },
|
|
56
|
+
{ name: "delay", type: "void", kind: "function", detail: "void delay(double seconds, void (*callback)())" },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const catalogCache = new Map();
|
|
60
|
+
|
|
61
|
+
function posix(file) {
|
|
62
|
+
return String(file).replace(/\\/g, "/");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function collectFiles(dir, files = []) {
|
|
66
|
+
if (!fs.existsSync(dir)) {
|
|
67
|
+
return files;
|
|
68
|
+
}
|
|
69
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
70
|
+
const full = path.join(dir, entry.name);
|
|
71
|
+
if (entry.isDirectory()) {
|
|
72
|
+
collectFiles(full, files);
|
|
73
|
+
} else {
|
|
74
|
+
files.push(full);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return files;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isCppFile(file) {
|
|
81
|
+
return /\.(cpp|cc|cxx|c|h|hpp|hh)$/i.test(file);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function cleanTypeName(name) {
|
|
85
|
+
if (!name) {
|
|
86
|
+
return "auto";
|
|
87
|
+
}
|
|
88
|
+
return String(name)
|
|
89
|
+
.replace(/^::/, "")
|
|
90
|
+
.replace(/\s+/g, "")
|
|
91
|
+
.replace(/\*+$/, "")
|
|
92
|
+
.replace(/&+$/, "")
|
|
93
|
+
.replace(/^const/, "");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function ensureType(types, name, base) {
|
|
97
|
+
const key = cleanTypeName(name);
|
|
98
|
+
if (!key) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
let entry = types.get(key);
|
|
102
|
+
if (!entry) {
|
|
103
|
+
entry = { name: key, base: base || null, members: new Map(), nested: new Map() };
|
|
104
|
+
types.set(key, entry);
|
|
105
|
+
} else if (base && !entry.base) {
|
|
106
|
+
entry.base = base;
|
|
107
|
+
}
|
|
108
|
+
return entry;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function addMember(typeEntry, member) {
|
|
112
|
+
if (!typeEntry || !member || !member.name) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const existing = typeEntry.members.get(member.name);
|
|
116
|
+
if (!existing || (member.kind === "method" && existing.kind !== "method")) {
|
|
117
|
+
typeEntry.members.set(member.name, member);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function tokenAt(tokens, i) {
|
|
122
|
+
return tokens[i] || tokens[tokens.length - 1];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function indexCpp(source, types, globals, options = {}) {
|
|
126
|
+
const tokens = tokenize(source);
|
|
127
|
+
let i = 0;
|
|
128
|
+
const ownerStack = [];
|
|
129
|
+
const namespaceStack = [];
|
|
130
|
+
const templateFields = options.templateFields || [];
|
|
131
|
+
|
|
132
|
+
const peek = (offset = 0) => tokenAt(tokens, i + offset);
|
|
133
|
+
const at = (type, value) => {
|
|
134
|
+
const token = peek();
|
|
135
|
+
if (type && token.type !== type) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
if (value !== undefined && token.value !== value) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
};
|
|
143
|
+
const eat = () => {
|
|
144
|
+
const token = peek();
|
|
145
|
+
i += 1;
|
|
146
|
+
return token;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const skipBalanced = (open, close) => {
|
|
150
|
+
if (!at("op", open)) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
let depth = 0;
|
|
154
|
+
while (!at("eof")) {
|
|
155
|
+
if (at("op", open)) {
|
|
156
|
+
depth += 1;
|
|
157
|
+
} else if (close === ">" && at("op", ">>")) {
|
|
158
|
+
depth -= 2;
|
|
159
|
+
eat();
|
|
160
|
+
if (depth <= 0) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
} else if (at("op", close)) {
|
|
165
|
+
depth -= 1;
|
|
166
|
+
eat();
|
|
167
|
+
if (depth === 0) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
eat();
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const skipAngle = () => skipBalanced("<", ">");
|
|
177
|
+
const skipParen = () => skipBalanced("(", ")");
|
|
178
|
+
const skipBrace = () => skipBalanced("{", "}");
|
|
179
|
+
|
|
180
|
+
const parseType = () => {
|
|
181
|
+
while (at("kw", "const") || at("kw", "static") || at("ident", "unsigned") || at("ident", "signed") || at("ident", "long") || at("ident", "short")) {
|
|
182
|
+
eat();
|
|
183
|
+
}
|
|
184
|
+
if (at("op", "::")) {
|
|
185
|
+
eat();
|
|
186
|
+
}
|
|
187
|
+
let name = "auto";
|
|
188
|
+
if (at("kw", "void") || at("kw", "int") || at("kw", "bool") || at("kw", "float") || at("kw", "double") || at("kw", "auto")) {
|
|
189
|
+
name = eat().value;
|
|
190
|
+
} else if (at("ident")) {
|
|
191
|
+
name = eat().value;
|
|
192
|
+
} else {
|
|
193
|
+
return { name: "auto", element: null };
|
|
194
|
+
}
|
|
195
|
+
while (at("op", "::")) {
|
|
196
|
+
eat();
|
|
197
|
+
if (at("ident") || at("kw")) {
|
|
198
|
+
name = eat().value;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
let element = null;
|
|
202
|
+
if (at("op", "<")) {
|
|
203
|
+
eat();
|
|
204
|
+
const inner = parseType();
|
|
205
|
+
element = inner.name;
|
|
206
|
+
while (!at("eof") && !at("op", ">") && !at("op", ">>")) {
|
|
207
|
+
eat();
|
|
208
|
+
}
|
|
209
|
+
if (at("op", ">>")) {
|
|
210
|
+
eat();
|
|
211
|
+
} else if (at("op", ">")) {
|
|
212
|
+
eat();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
while (at("op", "*") || at("op", "&")) {
|
|
216
|
+
eat();
|
|
217
|
+
}
|
|
218
|
+
return { name: cleanTypeName(name), element: element ? cleanTypeName(element) : null };
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const parseParams = () => {
|
|
222
|
+
const params = [];
|
|
223
|
+
if (!at("op", "(")) {
|
|
224
|
+
return params;
|
|
225
|
+
}
|
|
226
|
+
eat();
|
|
227
|
+
while (!at("eof") && !at("op", ")")) {
|
|
228
|
+
if (at("op", ",")) {
|
|
229
|
+
eat();
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const saved = i;
|
|
233
|
+
const type = parseType();
|
|
234
|
+
let name = "";
|
|
235
|
+
if (at("ident")) {
|
|
236
|
+
name = eat().value;
|
|
237
|
+
}
|
|
238
|
+
if (at("op", "=")) {
|
|
239
|
+
while (!at("eof") && !at("op", ",") && !at("op", ")")) {
|
|
240
|
+
if (at("op", "(")) {
|
|
241
|
+
skipParen();
|
|
242
|
+
} else {
|
|
243
|
+
eat();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (type.name && type.name !== "auto") {
|
|
248
|
+
params.push({ name, type: type.name });
|
|
249
|
+
} else if (saved === i) {
|
|
250
|
+
eat();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (at("op", ")")) {
|
|
254
|
+
eat();
|
|
255
|
+
}
|
|
256
|
+
return params;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const currentType = () => (ownerStack.length ? ownerStack[ownerStack.length - 1] : null);
|
|
260
|
+
|
|
261
|
+
const addGlobal = (item) => {
|
|
262
|
+
if (!item || !item.name) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
globals.set(item.name, item);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
while (!at("eof")) {
|
|
269
|
+
if (at("kw", "using") || at("kw", "typedef") || at("kw", "extern") || at("kw", "template") || at("kw", "enum")) {
|
|
270
|
+
if (at("kw", "enum")) {
|
|
271
|
+
eat();
|
|
272
|
+
if (at("kw", "class") || at("kw", "struct")) {
|
|
273
|
+
eat();
|
|
274
|
+
}
|
|
275
|
+
const enumName = at("ident") ? eat().value : null;
|
|
276
|
+
if (enumName) {
|
|
277
|
+
ensureType(types, enumName);
|
|
278
|
+
const parent = currentType();
|
|
279
|
+
if (parent) {
|
|
280
|
+
parent.nested.set(enumName, enumName);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (at("op", "{")) {
|
|
284
|
+
eat();
|
|
285
|
+
let enumType = enumName ? ensureType(types, `Enum${enumName}`) : null;
|
|
286
|
+
if (enumName) {
|
|
287
|
+
const enumNs = ensureType(types, "Enum");
|
|
288
|
+
addMember(enumNs, { name: enumName, kind: "enum", type: enumName, detail: `Enum::${enumName}`, static: true });
|
|
289
|
+
enumType = ensureType(types, enumName);
|
|
290
|
+
}
|
|
291
|
+
while (!at("eof") && !at("op", "}")) {
|
|
292
|
+
if (at("ident")) {
|
|
293
|
+
const item = eat().value;
|
|
294
|
+
if (enumType) {
|
|
295
|
+
addMember(enumType, { name: item, kind: "enum", type: enumName, detail: `${enumName}::${item}`, static: true });
|
|
296
|
+
}
|
|
297
|
+
while (!at("eof") && !at("op", ",") && !at("op", "}")) {
|
|
298
|
+
eat();
|
|
299
|
+
}
|
|
300
|
+
if (at("op", ",")) {
|
|
301
|
+
eat();
|
|
302
|
+
}
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
eat();
|
|
306
|
+
}
|
|
307
|
+
if (at("op", "}")) {
|
|
308
|
+
eat();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
while (!at("eof") && !at("op", ";") && !at("kw", "struct") && !at("kw", "class") && !at("kw", "namespace")) {
|
|
312
|
+
if (at("op", "{")) {
|
|
313
|
+
skipBrace();
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
eat();
|
|
317
|
+
}
|
|
318
|
+
if (at("op", ";")) {
|
|
319
|
+
eat();
|
|
320
|
+
}
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (at("kw", "template")) {
|
|
324
|
+
eat();
|
|
325
|
+
skipAngle();
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (at("kw", "extern")) {
|
|
329
|
+
eat();
|
|
330
|
+
const type = parseType();
|
|
331
|
+
if (at("ident")) {
|
|
332
|
+
const name = eat().value;
|
|
333
|
+
addGlobal({ name, type: type.name, kind: "variable", detail: `${type.name}* ${name}` });
|
|
334
|
+
}
|
|
335
|
+
while (!at("eof") && !at("op", ";")) {
|
|
336
|
+
eat();
|
|
337
|
+
}
|
|
338
|
+
if (at("op", ";")) {
|
|
339
|
+
eat();
|
|
340
|
+
}
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
while (!at("eof") && !at("op", ";") && !at("op", "{")) {
|
|
344
|
+
eat();
|
|
345
|
+
}
|
|
346
|
+
if (at("op", "{")) {
|
|
347
|
+
skipBrace();
|
|
348
|
+
} else if (at("op", ";")) {
|
|
349
|
+
eat();
|
|
350
|
+
}
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (at("kw", "namespace")) {
|
|
355
|
+
eat();
|
|
356
|
+
const name = at("ident") ? eat().value : "_anon";
|
|
357
|
+
const typeEntry = ensureType(types, name);
|
|
358
|
+
if (at("op", "{")) {
|
|
359
|
+
eat();
|
|
360
|
+
ownerStack.push(typeEntry);
|
|
361
|
+
namespaceStack.push(name);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (at("kw", "struct") || at("kw", "class")) {
|
|
368
|
+
eat();
|
|
369
|
+
const name = at("ident") ? eat().value : "_anon";
|
|
370
|
+
let base = null;
|
|
371
|
+
if (at("op", ":")) {
|
|
372
|
+
eat();
|
|
373
|
+
while (at("kw", "public") || at("kw", "private") || at("kw", "protected")) {
|
|
374
|
+
eat();
|
|
375
|
+
}
|
|
376
|
+
if (at("op", "::")) {
|
|
377
|
+
eat();
|
|
378
|
+
}
|
|
379
|
+
if (at("ident")) {
|
|
380
|
+
base = eat().value;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const typeEntry = ensureType(types, name, base);
|
|
384
|
+
if (at("op", ";")) {
|
|
385
|
+
eat();
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (at("op", "{")) {
|
|
389
|
+
eat();
|
|
390
|
+
ownerStack.push(typeEntry);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (at("kw", "public") || at("kw", "private") || at("kw", "protected")) {
|
|
397
|
+
eat();
|
|
398
|
+
if (at("op", ":")) {
|
|
399
|
+
eat();
|
|
400
|
+
}
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (at("op", "}")) {
|
|
405
|
+
eat();
|
|
406
|
+
const finished = ownerStack.pop();
|
|
407
|
+
namespaceStack.pop();
|
|
408
|
+
if (at("ident") && finished) {
|
|
409
|
+
const instanceName = eat().value;
|
|
410
|
+
const parent = currentType();
|
|
411
|
+
if (parent) {
|
|
412
|
+
addMember(parent, {
|
|
413
|
+
name: instanceName,
|
|
414
|
+
kind: "property",
|
|
415
|
+
type: finished.name,
|
|
416
|
+
detail: `${finished.name} ${instanceName}`,
|
|
417
|
+
nested: true,
|
|
418
|
+
});
|
|
419
|
+
parent.nested.set(instanceName, finished.name);
|
|
420
|
+
}
|
|
421
|
+
templateFields.push({ owner: parent ? parent.name : null, name: instanceName, type: finished.name, nested: true });
|
|
422
|
+
}
|
|
423
|
+
if (at("op", ";")) {
|
|
424
|
+
eat();
|
|
425
|
+
}
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (at("op", ";")) {
|
|
430
|
+
eat();
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const saved = i;
|
|
435
|
+
const isStatic = at("kw", "static") || (peek().type === "ident" && peek().value === "static");
|
|
436
|
+
if (at("kw", "static") || at("ident", "static")) {
|
|
437
|
+
eat();
|
|
438
|
+
}
|
|
439
|
+
if (at("ident", "operator") || (at("ident") && peek().value === "operator")) {
|
|
440
|
+
eat();
|
|
441
|
+
const op = at("op") ? eat().value : "";
|
|
442
|
+
const owner = currentType();
|
|
443
|
+
parseParams();
|
|
444
|
+
if (at("op", "{")) {
|
|
445
|
+
skipBrace();
|
|
446
|
+
} else if (at("op", ";")) {
|
|
447
|
+
eat();
|
|
448
|
+
}
|
|
449
|
+
if (owner) {
|
|
450
|
+
addMember(owner, {
|
|
451
|
+
name: `operator${op}`,
|
|
452
|
+
kind: "method",
|
|
453
|
+
type: owner.name,
|
|
454
|
+
detail: `${owner.name}& operator${op}`,
|
|
455
|
+
static: Boolean(isStatic),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const type = parseType();
|
|
462
|
+
if (at("ident") && peek(1).value === "(") {
|
|
463
|
+
const name = eat().value;
|
|
464
|
+
const params = parseParams();
|
|
465
|
+
const owner = currentType();
|
|
466
|
+
const signature = `${type.name} ${name}(${params.map((param) => param.type + (param.name ? " " + param.name : "")).join(", ")})`;
|
|
467
|
+
const member = {
|
|
468
|
+
name,
|
|
469
|
+
kind: name === (owner && owner.name) ? "constructor" : "method",
|
|
470
|
+
type: type.name,
|
|
471
|
+
element: type.element,
|
|
472
|
+
params,
|
|
473
|
+
detail: signature,
|
|
474
|
+
static: Boolean(isStatic) || Boolean(namespaceStack.length && owner && owner.name === namespaceStack[namespaceStack.length - 1]),
|
|
475
|
+
};
|
|
476
|
+
if (owner) {
|
|
477
|
+
addMember(owner, member);
|
|
478
|
+
} else {
|
|
479
|
+
addGlobal({ name, type: type.name, kind: "function", detail: signature, params });
|
|
480
|
+
}
|
|
481
|
+
if (at("op", "{")) {
|
|
482
|
+
skipBrace();
|
|
483
|
+
} else if (at("op", ";")) {
|
|
484
|
+
eat();
|
|
485
|
+
}
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (at("ident") && peek(1).value === "::" && peek(3) && peek(3).value === "(") {
|
|
490
|
+
const ownerName = eat().value;
|
|
491
|
+
eat();
|
|
492
|
+
const name = at("ident") ? eat().value : "";
|
|
493
|
+
const params = parseParams();
|
|
494
|
+
ensureType(types, ownerName);
|
|
495
|
+
const owner = types.get(cleanTypeName(ownerName));
|
|
496
|
+
addMember(owner, {
|
|
497
|
+
name,
|
|
498
|
+
kind: "method",
|
|
499
|
+
type: type.name,
|
|
500
|
+
element: type.element,
|
|
501
|
+
params,
|
|
502
|
+
detail: `${type.name} ${ownerName}::${name}()`,
|
|
503
|
+
});
|
|
504
|
+
if (at("op", "{")) {
|
|
505
|
+
skipBrace();
|
|
506
|
+
}
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (at("ident")) {
|
|
511
|
+
const name = eat().value;
|
|
512
|
+
if (at("op", "=")) {
|
|
513
|
+
while (!at("eof") && !at("op", ";")) {
|
|
514
|
+
eat();
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const owner = currentType();
|
|
518
|
+
const member = {
|
|
519
|
+
name,
|
|
520
|
+
kind: type.name === "RBXScriptSignal" ? "event" : "property",
|
|
521
|
+
type: type.name,
|
|
522
|
+
element: type.element,
|
|
523
|
+
detail: `${type.name} ${name}`,
|
|
524
|
+
static: Boolean(isStatic),
|
|
525
|
+
};
|
|
526
|
+
if (owner) {
|
|
527
|
+
addMember(owner, member);
|
|
528
|
+
} else {
|
|
529
|
+
addGlobal({ name, type: type.name, kind: "variable", detail: member.detail });
|
|
530
|
+
}
|
|
531
|
+
if (at("op", ";")) {
|
|
532
|
+
eat();
|
|
533
|
+
}
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (i === saved) {
|
|
538
|
+
eat();
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return { types, globals };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function headerFiles(includeDir) {
|
|
546
|
+
const root = path.join(includeDir, "cluaupp");
|
|
547
|
+
if (!fs.existsSync(root)) {
|
|
548
|
+
return [];
|
|
549
|
+
}
|
|
550
|
+
return collectFiles(root).filter((file) => isCppFile(file));
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function catalogKey(projectRoot) {
|
|
554
|
+
const includeDir = path.join(projectRoot, "include");
|
|
555
|
+
const fallback = path.join(PACKAGE_ROOT, "include");
|
|
556
|
+
const dir = fs.existsSync(includeDir) ? includeDir : fallback;
|
|
557
|
+
let stamp = dir;
|
|
558
|
+
for (const file of headerFiles(dir).slice(0, 8)) {
|
|
559
|
+
try {
|
|
560
|
+
stamp += ":" + fs.statSync(file).mtimeMs;
|
|
561
|
+
} catch {
|
|
562
|
+
// ignore
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return dir + stamp;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function loadCatalog(projectRoot) {
|
|
569
|
+
const includeDir = fs.existsSync(path.join(projectRoot, "include", "cluaupp"))
|
|
570
|
+
? path.join(projectRoot, "include")
|
|
571
|
+
: path.join(PACKAGE_ROOT, "include");
|
|
572
|
+
const key = catalogKey(projectRoot);
|
|
573
|
+
const cached = catalogCache.get(key);
|
|
574
|
+
if (cached) {
|
|
575
|
+
return cached;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const types = new Map();
|
|
579
|
+
const globals = new Map();
|
|
580
|
+
for (const item of GLOBALS) {
|
|
581
|
+
globals.set(item.name, item);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
for (const file of headerFiles(includeDir)) {
|
|
585
|
+
try {
|
|
586
|
+
indexCpp(fs.readFileSync(file, "utf8"), types, globals);
|
|
587
|
+
} catch {
|
|
588
|
+
// Header stubs must not kill IntelliSense.
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
for (const name of INSTANCE_TYPES) {
|
|
593
|
+
ensureType(types, name, name === "Instance" ? "Object" : types.has(name) ? types.get(name).base : "Instance");
|
|
594
|
+
}
|
|
595
|
+
for (const name of SERVICES) {
|
|
596
|
+
ensureType(types, name, types.has(name) ? types.get(name).base : "Instance");
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const catalog = { types, globals, includeDir };
|
|
600
|
+
catalogCache.set(key, catalog);
|
|
601
|
+
return catalog;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function indexProject(projectRoot) {
|
|
605
|
+
const catalog = loadCatalog(projectRoot);
|
|
606
|
+
const types = new Map(catalog.types);
|
|
607
|
+
const globals = new Map(catalog.globals);
|
|
608
|
+
const structs = [];
|
|
609
|
+
const srcDir = path.join(projectRoot, "src");
|
|
610
|
+
if (fs.existsSync(srcDir)) {
|
|
611
|
+
for (const file of collectFiles(srcDir).filter((entry) => isCppFile(entry))) {
|
|
612
|
+
try {
|
|
613
|
+
indexCpp(fs.readFileSync(file, "utf8"), types, globals);
|
|
614
|
+
} catch {
|
|
615
|
+
// Incomplete buffers are indexed best-effort.
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
for (const [name, type] of types) {
|
|
620
|
+
if (!catalog.types.has(name) || type.nested.size) {
|
|
621
|
+
structs.push(type);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
const template = types.get("TemplateData") || null;
|
|
625
|
+
return { types, globals, template, includeDir: catalog.includeDir, srcDir };
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function membersOf(index, typeName, seen = new Set()) {
|
|
629
|
+
const name = cleanTypeName(typeName);
|
|
630
|
+
if (!name || seen.has(name)) {
|
|
631
|
+
return [];
|
|
632
|
+
}
|
|
633
|
+
seen.add(name);
|
|
634
|
+
const type = index.types.get(name);
|
|
635
|
+
if (!type) {
|
|
636
|
+
return [];
|
|
637
|
+
}
|
|
638
|
+
const list = [...type.members.values()];
|
|
639
|
+
if (type.base) {
|
|
640
|
+
list.push(...membersOf(index, type.base, seen));
|
|
641
|
+
}
|
|
642
|
+
return list;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function pathMembers(index, nodeName) {
|
|
646
|
+
const type = index.types.get(cleanTypeName(nodeName || "TemplateData"));
|
|
647
|
+
if (!type) {
|
|
648
|
+
return [];
|
|
649
|
+
}
|
|
650
|
+
return [...type.members.values()].filter((member) => member.kind === "property");
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function lineAtOffset(source, offset) {
|
|
654
|
+
const start = source.lastIndexOf("\n", Math.max(0, offset - 1)) + 1;
|
|
655
|
+
return { start, text: source.slice(start, offset) };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function tokenIndexAt(tokens, offset) {
|
|
659
|
+
let found = 0;
|
|
660
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
661
|
+
const token = tokens[i];
|
|
662
|
+
if (typeof token.start !== "number") {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
if (token.start <= offset && offset <= token.end) {
|
|
666
|
+
found = i;
|
|
667
|
+
}
|
|
668
|
+
if (token.start > offset) {
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
found = i;
|
|
672
|
+
}
|
|
673
|
+
return found;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function skipCallBack(tokens, i) {
|
|
677
|
+
let index = i;
|
|
678
|
+
while (index >= 0 && tokens[index] && tokens[index].value === ")") {
|
|
679
|
+
let depth = 0;
|
|
680
|
+
while (index >= 0) {
|
|
681
|
+
const token = tokens[index];
|
|
682
|
+
if (token.value === ")") {
|
|
683
|
+
depth += 1;
|
|
684
|
+
} else if (token.value === "(") {
|
|
685
|
+
depth -= 1;
|
|
686
|
+
if (depth === 0) {
|
|
687
|
+
index -= 1;
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
index -= 1;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (index >= 0 && tokens[index] && tokens[index].value === ">") {
|
|
695
|
+
let depth = 0;
|
|
696
|
+
while (index >= 0) {
|
|
697
|
+
const token = tokens[index];
|
|
698
|
+
if (token.value === ">") {
|
|
699
|
+
depth += 1;
|
|
700
|
+
} else if (token.value === "<") {
|
|
701
|
+
depth -= 1;
|
|
702
|
+
if (depth === 0) {
|
|
703
|
+
index -= 1;
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
index -= 1;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return index;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function chainBefore(tokens, opIndex) {
|
|
714
|
+
const chain = [];
|
|
715
|
+
let i = opIndex;
|
|
716
|
+
while (i >= 0) {
|
|
717
|
+
const token = tokens[i];
|
|
718
|
+
if (!token || token.type === "eof") {
|
|
719
|
+
break;
|
|
720
|
+
}
|
|
721
|
+
if (token.value === "." || token.value === "->" || token.value === "::") {
|
|
722
|
+
i -= 1;
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
i = skipCallBack(tokens, i);
|
|
726
|
+
const current = tokens[i];
|
|
727
|
+
if (!current || (current.type !== "ident" && current.type !== "kw")) {
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
chain.unshift(current.value);
|
|
731
|
+
i -= 1;
|
|
732
|
+
const access = tokens[i];
|
|
733
|
+
if (!access || (access.value !== "." && access.value !== "->" && access.value !== "::")) {
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
i -= 1;
|
|
737
|
+
}
|
|
738
|
+
return chain;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function enclosingOwner(tokens, index) {
|
|
742
|
+
for (let i = index; i >= 1; i -= 1) {
|
|
743
|
+
if (tokens[i] && tokens[i].value === "::" && tokens[i - 1] && tokens[i - 1].type === "ident") {
|
|
744
|
+
const next = tokens[i + 1];
|
|
745
|
+
if (next && next.type === "ident") {
|
|
746
|
+
let j = i + 2;
|
|
747
|
+
while (tokens[j] && tokens[j].value !== "{" && tokens[j].value !== ";") {
|
|
748
|
+
if (tokens[j].value === "(") {
|
|
749
|
+
return tokens[i - 1].value;
|
|
750
|
+
}
|
|
751
|
+
j += 1;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (tokens[i] && (tokens[i].value === "struct" || tokens[i].value === "class") && tokens[i + 1] && tokens[i + 1].type === "ident") {
|
|
756
|
+
return tokens[i + 1].value;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
function inferRhsType(tokens, start) {
|
|
763
|
+
for (let i = start; i < tokens.length && tokens[i] && tokens[i].value !== ";"; i += 1) {
|
|
764
|
+
if (tokens[i].value === "GetService" && tokens[i + 1] && tokens[i + 1].value === "<" && tokens[i + 2] && tokens[i + 2].type === "ident") {
|
|
765
|
+
return tokens[i + 2].value;
|
|
766
|
+
}
|
|
767
|
+
if (tokens[i].value === "new" && tokens[i + 1] && tokens[i + 1].type === "ident") {
|
|
768
|
+
return tokens[i + 1].value;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function localsBefore(tokens, index) {
|
|
775
|
+
const locals = new Map();
|
|
776
|
+
for (let i = 0; i < index; i += 1) {
|
|
777
|
+
const typeTok = tokens[i];
|
|
778
|
+
if (!typeTok) {
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
const isType = typeTok.type === "ident" || ["int", "bool", "float", "double", "auto", "void"].includes(typeTok.value);
|
|
782
|
+
if (!isType) {
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
if (typeTok.value === "struct" || typeTok.value === "class" || typeTok.value === "namespace") {
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
let cursor = i + 1;
|
|
789
|
+
while (tokens[cursor] && (tokens[cursor].value === "*" || tokens[cursor].value === "&")) {
|
|
790
|
+
cursor += 1;
|
|
791
|
+
}
|
|
792
|
+
const ident = tokens[cursor];
|
|
793
|
+
if (!ident || ident.type !== "ident") {
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
if (tokens[cursor + 1] && tokens[cursor + 1].value === "(" && typeTok.value !== "auto") {
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
let typeName = cleanTypeName(typeTok.value);
|
|
800
|
+
if (typeName === "auto") {
|
|
801
|
+
typeName = inferRhsType(tokens, cursor) || typeName;
|
|
802
|
+
}
|
|
803
|
+
locals.set(ident.value, { name: ident.value, type: typeName, kind: "variable" });
|
|
804
|
+
}
|
|
805
|
+
return locals;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function inferGetService(tokens, index) {
|
|
809
|
+
for (let i = index; i >= 2; i -= 1) {
|
|
810
|
+
if (tokens[i].value === ">" && tokens[i - 2] && tokens[i - 2].value === "GetService") {
|
|
811
|
+
const inner = tokens[i - 1];
|
|
812
|
+
if (inner && inner.type === "ident") {
|
|
813
|
+
return inner.value;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function lookupMember(index, typeName, name) {
|
|
821
|
+
return membersOf(index, typeName).find((member) => member.name === name) || null;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function inferChain(index, chain, owner, locals) {
|
|
825
|
+
if (!chain.length) {
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
let current = null;
|
|
829
|
+
let pathNode = null;
|
|
830
|
+
const first = chain[0];
|
|
831
|
+
if (first === "this" && owner) {
|
|
832
|
+
current = owner;
|
|
833
|
+
} else if (index.types.has(first) && (chain.length === 1 || true)) {
|
|
834
|
+
const local = locals.get(first);
|
|
835
|
+
if (local) {
|
|
836
|
+
current = local.type;
|
|
837
|
+
} else if (index.globals.has(first)) {
|
|
838
|
+
current = index.globals.get(first).type;
|
|
839
|
+
} else if (owner) {
|
|
840
|
+
const field = lookupMember(index, owner, first);
|
|
841
|
+
current = field ? field.type : first;
|
|
842
|
+
} else {
|
|
843
|
+
current = first;
|
|
844
|
+
}
|
|
845
|
+
} else if (locals.has(first)) {
|
|
846
|
+
current = locals.get(first).type;
|
|
847
|
+
} else if (index.globals.has(first)) {
|
|
848
|
+
current = index.globals.get(first).type;
|
|
849
|
+
} else if (owner) {
|
|
850
|
+
const field = lookupMember(index, owner, first);
|
|
851
|
+
current = field ? field.type : null;
|
|
852
|
+
}
|
|
853
|
+
if (!current) {
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
if (current === "DataPath" && index.template) {
|
|
857
|
+
pathNode = "TemplateData";
|
|
858
|
+
}
|
|
859
|
+
if (first === "DataService") {
|
|
860
|
+
current = "DataService";
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
for (let i = 1; i < chain.length; i += 1) {
|
|
864
|
+
const name = chain[i];
|
|
865
|
+
if (current === "DataPath" || pathNode) {
|
|
866
|
+
const props = pathMembers(index, pathNode || "TemplateData");
|
|
867
|
+
const prop = props.find((item) => item.name === name);
|
|
868
|
+
if (prop) {
|
|
869
|
+
const nested = pathMembers(index, prop.type);
|
|
870
|
+
if (nested.length && !["int", "string", "bool", "double", "float"].includes(prop.type)) {
|
|
871
|
+
current = "DataPath";
|
|
872
|
+
pathNode = prop.type;
|
|
873
|
+
} else {
|
|
874
|
+
current = prop.type;
|
|
875
|
+
pathNode = null;
|
|
876
|
+
}
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
const member = lookupMember(index, current, name);
|
|
881
|
+
if (!member) {
|
|
882
|
+
if (name === "Paths") {
|
|
883
|
+
current = "DataPath";
|
|
884
|
+
pathNode = "TemplateData";
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
return { type: current, pathNode };
|
|
888
|
+
}
|
|
889
|
+
if (member.element && member.type === "LuaArray") {
|
|
890
|
+
current = "LuaArray";
|
|
891
|
+
} else {
|
|
892
|
+
current = member.type;
|
|
893
|
+
}
|
|
894
|
+
if (current === "DataPath") {
|
|
895
|
+
pathNode = pathNode || "TemplateData";
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return { type: current, pathNode, element: index.types.get(current) ? null : null };
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function filterPrefix(items, prefix) {
|
|
902
|
+
if (!prefix) {
|
|
903
|
+
return items;
|
|
904
|
+
}
|
|
905
|
+
const lower = prefix.toLowerCase();
|
|
906
|
+
return items.filter((item) => item.name.toLowerCase().startsWith(lower));
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function uniqueItems(items) {
|
|
910
|
+
const seen = new Set();
|
|
911
|
+
const out = [];
|
|
912
|
+
for (const item of items) {
|
|
913
|
+
const key = item.kind + ":" + item.name;
|
|
914
|
+
if (seen.has(key)) {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
seen.add(key);
|
|
918
|
+
out.push(item);
|
|
919
|
+
}
|
|
920
|
+
return out;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function includeCompletions(source, offset, index) {
|
|
924
|
+
const { text } = lineAtOffset(source, offset);
|
|
925
|
+
const match = text.match(/^\s*#\s*include\s+(["<])([^">]*)$/);
|
|
926
|
+
if (!match) {
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
const quote = match[1];
|
|
930
|
+
const prefix = match[2] || "";
|
|
931
|
+
const items = [];
|
|
932
|
+
if (quote === "<") {
|
|
933
|
+
const root = path.join(index.includeDir, "cluaupp");
|
|
934
|
+
if (fs.existsSync(root)) {
|
|
935
|
+
for (const file of collectFiles(root)) {
|
|
936
|
+
const rel = "cluaupp/" + posix(path.relative(root, file));
|
|
937
|
+
items.push({ name: rel, kind: "file", type: "header", detail: `#include <${rel}>` });
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
items.push({ name: "cluaupp/roblox.hpp", kind: "file", type: "header", detail: "#include <cluaupp/roblox.hpp>" });
|
|
941
|
+
} else if (index.srcDir && fs.existsSync(index.srcDir)) {
|
|
942
|
+
for (const file of collectFiles(index.srcDir).filter((entry) => isCppFile(entry))) {
|
|
943
|
+
const rel = posix(path.relative(index.srcDir, file));
|
|
944
|
+
items.push({ name: rel, kind: "file", type: "header", detail: `#include "${rel}"` });
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return filterPrefix(items, prefix.split("/").pop() || prefix);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function completeAt(source, offset, options = {}) {
|
|
951
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
952
|
+
const index = indexProject(projectRoot);
|
|
953
|
+
const includeItems = includeCompletions(source, offset, index);
|
|
954
|
+
if (includeItems) {
|
|
955
|
+
return uniqueItems(includeItems);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const tokens = tokenize(source);
|
|
959
|
+
const indexTok = tokenIndexAt(tokens, offset);
|
|
960
|
+
const current = tokens[indexTok];
|
|
961
|
+
let prefix = "";
|
|
962
|
+
let opIndex = indexTok;
|
|
963
|
+
if (current && current.type === "ident" && current.start < offset) {
|
|
964
|
+
prefix = source.slice(current.start, offset);
|
|
965
|
+
opIndex = indexTok - 1;
|
|
966
|
+
} else if (current && (current.value === "." || current.value === "->" || current.value === "::" || current.value === "<")) {
|
|
967
|
+
prefix = "";
|
|
968
|
+
opIndex = indexTok;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
const owner = enclosingOwner(tokens, indexTok);
|
|
972
|
+
const locals = localsBefore(tokens, indexTok);
|
|
973
|
+
if (owner) {
|
|
974
|
+
for (const member of membersOf(index, owner)) {
|
|
975
|
+
if (member.kind === "property") {
|
|
976
|
+
locals.set(member.name, { name: member.name, type: member.type, kind: "property" });
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const op = tokens[opIndex];
|
|
982
|
+
if (op && op.value === "<") {
|
|
983
|
+
const ident = tokens[opIndex - 1];
|
|
984
|
+
if (ident && ident.value === "GetService") {
|
|
985
|
+
return uniqueItems(
|
|
986
|
+
filterPrefix(
|
|
987
|
+
[...SERVICES].map((name) => ({ name, kind: "class", type: name, detail: `GetService<${name}>()` })),
|
|
988
|
+
prefix,
|
|
989
|
+
),
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (op && (op.value === "." || op.value === "->" || op.value === "::")) {
|
|
995
|
+
const chain = chainBefore(tokens, opIndex - 1);
|
|
996
|
+
if (op.value === "::" && chain.length === 1 && index.types.has(chain[0])) {
|
|
997
|
+
const type = index.types.get(chain[0]);
|
|
998
|
+
const statics = [...type.members.values()].filter((member) => member.static || member.kind === "enum" || member.kind === "constructor");
|
|
999
|
+
const nested = [...type.nested.keys()].map((name) => ({ name, kind: "class", type: name, detail: `${chain[0]}::${name}` }));
|
|
1000
|
+
if (chain[0] === "Enum") {
|
|
1001
|
+
return uniqueItems(filterPrefix([...type.members.values(), ...nested], prefix));
|
|
1002
|
+
}
|
|
1003
|
+
if (chain[0] === "DataService") {
|
|
1004
|
+
return uniqueItems(
|
|
1005
|
+
filterPrefix(
|
|
1006
|
+
[
|
|
1007
|
+
{ name: "Server", kind: "property", type: "DataServiceServer", detail: "DataServiceServer Server", static: true },
|
|
1008
|
+
{ name: "Client", kind: "property", type: "DataServiceClient", detail: "DataServiceClient Client", static: true },
|
|
1009
|
+
{ name: "Paths", kind: "property", type: "DataPath", detail: "DataPath Paths", static: true },
|
|
1010
|
+
{ name: "Enum", kind: "property", type: "DataServiceEnum", detail: "DataServiceEnum Enum", static: true },
|
|
1011
|
+
...statics,
|
|
1012
|
+
...nested,
|
|
1013
|
+
],
|
|
1014
|
+
prefix,
|
|
1015
|
+
),
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
if (chain[0] === "FormatNumber" || statics.length || nested.length) {
|
|
1019
|
+
return uniqueItems(filterPrefix([...type.members.values(), ...nested], prefix));
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
const inferred = inferChain(index, chain, owner, locals);
|
|
1023
|
+
let typeName = inferred && inferred.type;
|
|
1024
|
+
if (!typeName || typeName === "auto" || (chain.length === 1 && !index.types.has(typeName))) {
|
|
1025
|
+
typeName = inferGetService(tokens, opIndex) || inferRhsType(tokens, 0) || typeName;
|
|
1026
|
+
}
|
|
1027
|
+
let items = [];
|
|
1028
|
+
if (typeName === "DataPath" || (inferred && inferred.pathNode)) {
|
|
1029
|
+
items = pathMembers(index, inferred && inferred.pathNode ? inferred.pathNode : "TemplateData").map((member) => ({
|
|
1030
|
+
...member,
|
|
1031
|
+
detail: `DataPath ${member.name}`,
|
|
1032
|
+
}));
|
|
1033
|
+
}
|
|
1034
|
+
if (!items.length) {
|
|
1035
|
+
items = membersOf(index, typeName);
|
|
1036
|
+
}
|
|
1037
|
+
if (op.value === "::") {
|
|
1038
|
+
items = items.filter((member) => member.static || member.kind === "enum" || member.kind === "constructor" || member.kind === "class");
|
|
1039
|
+
if (!items.length) {
|
|
1040
|
+
items = membersOf(index, typeName);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
return uniqueItems(filterPrefix(items, prefix));
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const items = [];
|
|
1047
|
+
for (const word of KEYWORDS) {
|
|
1048
|
+
items.push({ name: word, kind: "keyword", type: "keyword", detail: word });
|
|
1049
|
+
}
|
|
1050
|
+
for (const item of index.globals.values()) {
|
|
1051
|
+
items.push(item);
|
|
1052
|
+
}
|
|
1053
|
+
for (const item of locals.values()) {
|
|
1054
|
+
items.push({ ...item, detail: `${item.type} ${item.name}` });
|
|
1055
|
+
}
|
|
1056
|
+
if (owner) {
|
|
1057
|
+
items.push({ name: "this", kind: "variable", type: owner, detail: `${owner}* this` });
|
|
1058
|
+
items.push(...membersOf(index, owner));
|
|
1059
|
+
}
|
|
1060
|
+
for (const name of INSTANCE_TYPES) {
|
|
1061
|
+
items.push({ name, kind: "class", type: name, detail: `new ${name}(parent)` });
|
|
1062
|
+
}
|
|
1063
|
+
for (const name of SERVICES) {
|
|
1064
|
+
items.push({ name, kind: "class", type: name, detail: `GetService<${name}>()` });
|
|
1065
|
+
}
|
|
1066
|
+
for (const type of index.types.keys()) {
|
|
1067
|
+
items.push({ name: type, kind: "class", type, detail: type });
|
|
1068
|
+
}
|
|
1069
|
+
return uniqueItems(filterPrefix(items, prefix)).slice(0, 200);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function hoverAt(source, offset, options = {}) {
|
|
1073
|
+
const items = completeAt(source, offset, options);
|
|
1074
|
+
const tokens = tokenize(source);
|
|
1075
|
+
const token = tokens[tokenIndexAt(tokens, offset)];
|
|
1076
|
+
if (!token || token.type !== "ident") {
|
|
1077
|
+
return items[0] ? { name: items[0].name, detail: items[0].detail, type: items[0].type } : null;
|
|
1078
|
+
}
|
|
1079
|
+
const match = items.find((item) => item.name === token.value) || completeAt(source.slice(0, token.start) + source.slice(token.end), token.start, options).find((item) => item.name === token.value);
|
|
1080
|
+
if (match) {
|
|
1081
|
+
return { name: match.name, detail: match.detail, type: match.type, kind: match.kind };
|
|
1082
|
+
}
|
|
1083
|
+
return { name: token.value, detail: token.value, type: "auto" };
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function definitionAt(source, offset, options = {}) {
|
|
1087
|
+
const projectRoot = options.projectRoot || process.cwd();
|
|
1088
|
+
const file = options.file;
|
|
1089
|
+
const tokens = tokenize(source);
|
|
1090
|
+
const token = tokens[tokenIndexAt(tokens, offset)];
|
|
1091
|
+
if (!token) {
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
if (token.type === "string" && file) {
|
|
1095
|
+
const { text } = lineAtOffset(source, offset);
|
|
1096
|
+
if (/^\s*#\s*include/.test(text)) {
|
|
1097
|
+
const name = token.value;
|
|
1098
|
+
const index = indexProject(projectRoot);
|
|
1099
|
+
const quoted = path.resolve(path.dirname(file), name);
|
|
1100
|
+
const angled = path.join(index.includeDir, name);
|
|
1101
|
+
const target = fs.existsSync(quoted) ? quoted : fs.existsSync(angled) ? angled : null;
|
|
1102
|
+
if (target) {
|
|
1103
|
+
return { file: target, line: 1, col: 1 };
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
if (token.type !== "ident") {
|
|
1108
|
+
return null;
|
|
1109
|
+
}
|
|
1110
|
+
const srcDir = path.join(projectRoot, "src");
|
|
1111
|
+
if (!fs.existsSync(srcDir)) {
|
|
1112
|
+
return { file: file || srcDir, line: token.line, col: token.col };
|
|
1113
|
+
}
|
|
1114
|
+
for (const candidate of collectFiles(srcDir).filter((entry) => isCppFile(entry))) {
|
|
1115
|
+
const text = fs.readFileSync(candidate, "utf8");
|
|
1116
|
+
const match = text.match(new RegExp(`(?:struct|class)\\s+${token.value}\\b`)) || text.match(new RegExp(`\\b${token.value}\\s*\\(`));
|
|
1117
|
+
if (match) {
|
|
1118
|
+
const before = text.slice(0, match.index);
|
|
1119
|
+
const line = before.split(/\n/).length;
|
|
1120
|
+
return { file: candidate, line, col: 1 };
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
return { file: file || srcDir, line: token.line, col: token.col };
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function diagnosticsFor(source, fileName, options = {}) {
|
|
1127
|
+
try {
|
|
1128
|
+
const { parse } = require("./parse");
|
|
1129
|
+
const { preprocess } = require("./preprocess");
|
|
1130
|
+
const prepared = options.filePath ? preprocess(source, options.filePath, options) : source;
|
|
1131
|
+
parse(prepared, fileName || "input.cpp");
|
|
1132
|
+
return [];
|
|
1133
|
+
} catch (err) {
|
|
1134
|
+
const message = err && err.message ? String(err.message) : "parse error";
|
|
1135
|
+
const match = message.match(/:(\d+):(\d+):\s*(.*)$/);
|
|
1136
|
+
if (match) {
|
|
1137
|
+
return [{ line: Number(match[1]), col: Number(match[2]), message: match[3], severity: "error" }];
|
|
1138
|
+
}
|
|
1139
|
+
return [{ line: err.line || 1, col: err.col || 1, message, severity: "error" }];
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function intelliSenseMode() {
|
|
1144
|
+
const arch = process.arch === "arm64" ? "arm64" : "x64";
|
|
1145
|
+
if (process.platform === "win32") {
|
|
1146
|
+
return `windows-clang-${arch}`;
|
|
1147
|
+
}
|
|
1148
|
+
if (process.platform === "darwin") {
|
|
1149
|
+
return `macos-clang-${arch}`;
|
|
1150
|
+
}
|
|
1151
|
+
return `linux-clang-${arch}`;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function clangPath() {
|
|
1155
|
+
const windows = "C:/Program Files/LLVM/bin/clang++.exe";
|
|
1156
|
+
if (process.platform === "win32" && fs.existsSync(windows)) {
|
|
1157
|
+
return windows;
|
|
1158
|
+
}
|
|
1159
|
+
return "clang++";
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function compileCommandEntry(root, file) {
|
|
1163
|
+
const absRoot = posix(path.resolve(root));
|
|
1164
|
+
const absFile = posix(path.resolve(file));
|
|
1165
|
+
return {
|
|
1166
|
+
directory: absRoot,
|
|
1167
|
+
file: absFile,
|
|
1168
|
+
arguments: [
|
|
1169
|
+
clangPath(),
|
|
1170
|
+
"-xc++",
|
|
1171
|
+
"-std=c++20",
|
|
1172
|
+
"-ferror-limit=0",
|
|
1173
|
+
"-I" + posix(path.join(absRoot, "include")),
|
|
1174
|
+
"-I" + posix(path.join(absRoot, "src")),
|
|
1175
|
+
"-c",
|
|
1176
|
+
absFile,
|
|
1177
|
+
],
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function writeCompileCommands(root, srcDir) {
|
|
1182
|
+
const files = collectFiles(srcDir).filter((file) => isCppFile(file)).sort();
|
|
1183
|
+
const json = JSON.stringify(files.map((file) => compileCommandEntry(root, file)), null, "\t") + "\n";
|
|
1184
|
+
const dest = path.join(root, "compile_commands.json");
|
|
1185
|
+
if (fs.existsSync(dest) && fs.readFileSync(dest, "utf8") === json) {
|
|
1186
|
+
return false;
|
|
1187
|
+
}
|
|
1188
|
+
fs.writeFileSync(dest, json, "utf8");
|
|
1189
|
+
return true;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function writeClangd(root) {
|
|
1193
|
+
const absInclude = posix(path.resolve(root, "include"));
|
|
1194
|
+
const absSrc = posix(path.resolve(root, "src"));
|
|
1195
|
+
const contents = `CompileFlags:
|
|
1196
|
+
Add:
|
|
1197
|
+
- -xc++
|
|
1198
|
+
- -std=c++20
|
|
1199
|
+
- -ferror-limit=0
|
|
1200
|
+
- -I${absInclude}
|
|
1201
|
+
- -I${absSrc}
|
|
1202
|
+
- -Iinclude
|
|
1203
|
+
- -Isrc
|
|
1204
|
+
|
|
1205
|
+
Diagnostics:
|
|
1206
|
+
Suppress: '*'
|
|
1207
|
+
|
|
1208
|
+
---
|
|
1209
|
+
If:
|
|
1210
|
+
PathMatch: (out|libs)/.*
|
|
1211
|
+
Index:
|
|
1212
|
+
Background: Skip
|
|
1213
|
+
`;
|
|
1214
|
+
const dest = path.join(root, ".clangd");
|
|
1215
|
+
if (fs.existsSync(dest) && fs.readFileSync(dest, "utf8") === contents) {
|
|
1216
|
+
return false;
|
|
1217
|
+
}
|
|
1218
|
+
fs.writeFileSync(dest, contents, "utf8");
|
|
1219
|
+
return true;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function mergeJson(file, patch) {
|
|
1223
|
+
let current = {};
|
|
1224
|
+
if (fs.existsSync(file)) {
|
|
1225
|
+
try {
|
|
1226
|
+
current = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1227
|
+
} catch {
|
|
1228
|
+
current = {};
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
const next = { ...current, ...patch };
|
|
1232
|
+
for (const key of Object.keys(patch)) {
|
|
1233
|
+
if (patch[key] && typeof patch[key] === "object" && !Array.isArray(patch[key]) && current[key] && typeof current[key] === "object") {
|
|
1234
|
+
next[key] = { ...current[key], ...patch[key] };
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
const json = JSON.stringify(next, null, "\t") + "\n";
|
|
1238
|
+
if (fs.existsSync(file) && fs.readFileSync(file, "utf8") === json) {
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1242
|
+
fs.writeFileSync(file, json, "utf8");
|
|
1243
|
+
return true;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function writeVscode(root) {
|
|
1247
|
+
const settings = path.join(root, ".vscode", "settings.json");
|
|
1248
|
+
mergeJson(settings, {
|
|
1249
|
+
"C_Cpp.intelliSenseEngine": "default",
|
|
1250
|
+
"C_Cpp.default.cppStandard": "c++20",
|
|
1251
|
+
"C_Cpp.default.cStandard": "c17",
|
|
1252
|
+
"C_Cpp.default.compilerPath": clangPath(),
|
|
1253
|
+
"C_Cpp.default.intelliSenseMode": intelliSenseMode(),
|
|
1254
|
+
"C_Cpp.default.includePath": ["${workspaceFolder}/include", "${workspaceFolder}/src"],
|
|
1255
|
+
"clangd.enable": false,
|
|
1256
|
+
"files.associations": {
|
|
1257
|
+
"*.hpp": "cpp",
|
|
1258
|
+
"*.h": "cpp",
|
|
1259
|
+
"*.server.cpp": "cpp",
|
|
1260
|
+
"*.client.cpp": "cpp",
|
|
1261
|
+
"*.plugin.cpp": "cpp",
|
|
1262
|
+
"*.legacy.cpp": "cpp",
|
|
1263
|
+
"*.legacy.server.cpp": "cpp",
|
|
1264
|
+
"*.legacy.client.cpp": "cpp",
|
|
1265
|
+
},
|
|
1266
|
+
});
|
|
1267
|
+
mergeJson(path.join(root, ".vscode", "extensions.json"), {
|
|
1268
|
+
recommendations: ["ms-vscode.cpptools", "kartzdev.cluaupp-intellisense"],
|
|
1269
|
+
});
|
|
1270
|
+
const props = path.join(root, ".vscode", "c_cpp_properties.json");
|
|
1271
|
+
const nextProps = {
|
|
1272
|
+
configurations: [
|
|
1273
|
+
{
|
|
1274
|
+
name: "Cluaupp",
|
|
1275
|
+
compilerPath: clangPath(),
|
|
1276
|
+
cStandard: "c17",
|
|
1277
|
+
cppStandard: "c++20",
|
|
1278
|
+
intelliSenseMode: intelliSenseMode(),
|
|
1279
|
+
includePath: ["${workspaceFolder}/include", "${workspaceFolder}/src"],
|
|
1280
|
+
forcedInclude: ["${workspaceFolder}/include/cluaupp/roblox.hpp"],
|
|
1281
|
+
compileCommands: "${workspaceFolder}/compile_commands.json",
|
|
1282
|
+
browse: {
|
|
1283
|
+
path: ["${workspaceFolder}/include", "${workspaceFolder}/src"],
|
|
1284
|
+
limitSymbolsToIncludedHeaders: true,
|
|
1285
|
+
},
|
|
1286
|
+
},
|
|
1287
|
+
],
|
|
1288
|
+
version: 4,
|
|
1289
|
+
};
|
|
1290
|
+
const json = JSON.stringify(nextProps, null, "\t") + "\n";
|
|
1291
|
+
fs.mkdirSync(path.dirname(props), { recursive: true });
|
|
1292
|
+
if (!fs.existsSync(props) || fs.readFileSync(props, "utf8") !== json) {
|
|
1293
|
+
fs.writeFileSync(props, json, "utf8");
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
function copyDir(from, to) {
|
|
1298
|
+
fs.mkdirSync(to, { recursive: true });
|
|
1299
|
+
for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
|
|
1300
|
+
const src = path.join(from, entry.name);
|
|
1301
|
+
const dest = path.join(to, entry.name);
|
|
1302
|
+
if (entry.isDirectory()) {
|
|
1303
|
+
copyDir(src, dest);
|
|
1304
|
+
} else {
|
|
1305
|
+
fs.copyFileSync(src, dest);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
function installEditorExtension(cluauppRoot = PACKAGE_ROOT) {
|
|
1311
|
+
const from = path.join(cluauppRoot, "editors", "vscode");
|
|
1312
|
+
if (!fs.existsSync(from)) {
|
|
1313
|
+
return [];
|
|
1314
|
+
}
|
|
1315
|
+
const id = `kartzdev.cluaupp-intellisense-${pkg.version}`;
|
|
1316
|
+
const homes = [
|
|
1317
|
+
path.join(os.homedir(), ".cursor", "extensions"),
|
|
1318
|
+
path.join(os.homedir(), ".vscode", "extensions"),
|
|
1319
|
+
];
|
|
1320
|
+
const installed = [];
|
|
1321
|
+
for (const home of homes) {
|
|
1322
|
+
if (!fs.existsSync(path.dirname(home))) {
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
fs.mkdirSync(home, { recursive: true });
|
|
1326
|
+
const dest = path.join(home, id);
|
|
1327
|
+
copyDir(from, dest);
|
|
1328
|
+
fs.writeFileSync(path.join(dest, "cluaupp.root"), path.resolve(cluauppRoot), "utf8");
|
|
1329
|
+
installed.push(dest);
|
|
1330
|
+
}
|
|
1331
|
+
return installed;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function syncEditorSupport(root, config = {}) {
|
|
1335
|
+
const srcDir = path.join(root, config.rootDir || "src");
|
|
1336
|
+
if (fs.existsSync(srcDir)) {
|
|
1337
|
+
writeCompileCommands(root, srcDir);
|
|
1338
|
+
}
|
|
1339
|
+
writeClangd(root);
|
|
1340
|
+
writeVscode(root);
|
|
1341
|
+
const flags = "-xc++\n-std=c++20\n-ferror-limit=0\n-Iinclude\n-Isrc\n";
|
|
1342
|
+
const flagsFile = path.join(root, "compile_flags.txt");
|
|
1343
|
+
if (!fs.existsSync(flagsFile) || fs.readFileSync(flagsFile, "utf8") !== flags) {
|
|
1344
|
+
fs.writeFileSync(flagsFile, flags, "utf8");
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
async function installEditorSupport(cluauppRoot = PACKAGE_ROOT) {
|
|
1349
|
+
const { installCppTools, reportCppTools } = require("./editor-install");
|
|
1350
|
+
const local = installEditorExtension(cluauppRoot);
|
|
1351
|
+
const cpp = await installCppTools();
|
|
1352
|
+
reportCppTools(cpp);
|
|
1353
|
+
return { local, cpp };
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
module.exports = {
|
|
1357
|
+
completeAt,
|
|
1358
|
+
hoverAt,
|
|
1359
|
+
definitionAt,
|
|
1360
|
+
diagnosticsFor,
|
|
1361
|
+
indexProject,
|
|
1362
|
+
syncEditorSupport,
|
|
1363
|
+
installEditorExtension,
|
|
1364
|
+
installEditorSupport,
|
|
1365
|
+
writeCompileCommands,
|
|
1366
|
+
intelliSenseMode,
|
|
1367
|
+
clangPath,
|
|
1368
|
+
};
|