cluaupp 0.1.4 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +12 -7
  3. package/bin/cluau.js +1 -1
  4. package/bin/cluaupp.js +1 -1
  5. package/docs/README.md +1 -1
  6. package/docs/architecture.md +52 -77
  7. package/docs/cli.md +29 -9
  8. package/docs/comparison.md +3 -3
  9. package/docs/config.md +6 -6
  10. package/docs/cpp-organization.md +2 -2
  11. package/docs/examples/index.md +2 -0
  12. package/docs/getting-started.md +7 -5
  13. package/docs/intellisense.md +49 -31
  14. package/docs/intro.md +2 -2
  15. package/docs/oop/file-tags.md +16 -17
  16. package/docs/oop/index.md +2 -2
  17. package/docs/oop/modules.md +3 -1
  18. package/docs/oop/services.md +7 -14
  19. package/docs/syntax.md +6 -5
  20. package/editors/vscode/extension.js +97 -146
  21. package/editors/vscode/package.json +6 -6
  22. package/examples/README.md +18 -0
  23. package/examples/game/.clangd +25 -0
  24. package/examples/game/.vscode/c_cpp_properties.json +27 -0
  25. package/examples/game/.vscode/extensions.json +5 -0
  26. package/examples/game/.vscode/settings.json +27 -0
  27. package/examples/game/Packages/.gitkeep +0 -0
  28. package/examples/game/cluaupp.config.json +6 -0
  29. package/examples/game/compile_flags.txt +6 -0
  30. package/examples/game/default.project.json +39 -0
  31. package/examples/game/package.json +9 -0
  32. package/examples/game/rokit.toml +5 -0
  33. package/examples/game/src/client/hud.client.cpp +12 -0
  34. package/{src → examples/game/src}/client/init.client.cpp +1 -0
  35. package/examples/game/src/server/combat.server.cpp +23 -0
  36. package/{src → examples/game/src}/server/leaderstats.server.cpp +1 -0
  37. package/examples/game/wally.toml +11 -0
  38. package/generated/api.js +51 -0
  39. package/generated/architecture.js +473 -0
  40. package/generated/ast.js +2 -0
  41. package/generated/cli.js +191 -0
  42. package/generated/compile.js +115 -0
  43. package/generated/editor-install.js +170 -0
  44. package/generated/emit.js +503 -0
  45. package/generated/emitter/luau-codegen.js +167 -0
  46. package/generated/emitter/translators.js +164 -0
  47. package/generated/headers.js +220 -0
  48. package/generated/index.html +49 -0
  49. package/generated/intellisense.js +277 -0
  50. package/generated/layout.js +112 -0
  51. package/generated/lex.js +146 -0
  52. package/generated/libs.js +484 -0
  53. package/generated/lsp.js +55 -0
  54. package/generated/package-info.js +11 -0
  55. package/generated/parse.js +607 -0
  56. package/generated/parser/collector.js +100 -0
  57. package/generated/parser/index.js +30 -0
  58. package/generated/preprocess.js +181 -0
  59. package/generated/system-understander.js +443 -0
  60. package/generated/transpile.js +66 -0
  61. package/generated/types.js +2 -0
  62. package/generated/understand.js +298 -0
  63. package/generated/utils/process-orchestrator.js +63 -0
  64. package/generated/utils/project.js +491 -0
  65. package/generated/utils/rojo-mapper.js +181 -0
  66. package/generated/utils/safe-paths.js +104 -0
  67. package/package.json +25 -8
  68. package/src/api.ts +53 -0
  69. package/src/{architecture.js → architecture.ts} +80 -33
  70. package/src/ast.ts +37 -0
  71. package/src/cli.ts +214 -0
  72. package/src/compile.ts +118 -0
  73. package/src/editor-install.ts +182 -0
  74. package/src/{emit.js → emit.ts} +5 -6
  75. package/src/emitter/luau-codegen.ts +187 -0
  76. package/src/emitter/translators.ts +168 -0
  77. package/src/headers.ts +233 -0
  78. package/src/intellisense.ts +286 -0
  79. package/src/{layout.js → layout.ts} +114 -129
  80. package/src/{lex.js → lex.ts} +4 -6
  81. package/src/{libs.js → libs.ts} +119 -46
  82. package/src/lsp.ts +60 -0
  83. package/src/package-info.ts +7 -0
  84. package/src/{parse.js → parse.ts} +3 -4
  85. package/src/parser/collector.ts +115 -0
  86. package/src/parser/index.ts +27 -0
  87. package/src/{preprocess.js → preprocess.ts} +64 -32
  88. package/src/system-understander.ts +501 -0
  89. package/src/transpile.ts +64 -0
  90. package/src/tree-sitter-cpp.d.ts +4 -0
  91. package/src/types.ts +81 -0
  92. package/src/{understand.js → understand.ts} +24 -213
  93. package/src/utils/process-orchestrator.ts +72 -0
  94. package/src/utils/project.ts +506 -0
  95. package/src/utils/rojo-mapper.ts +190 -0
  96. package/src/utils/safe-paths.ts +98 -0
  97. package/templates/game/.clangd +9 -0
  98. package/templates/game/.vscode/c_cpp_properties.json +1 -1
  99. package/templates/game/.vscode/extensions.json +5 -6
  100. package/templates/game/.vscode/settings.json +7 -6
  101. package/templates/game/cluaupp.config.json +2 -2
  102. package/templates/game/compile_flags.txt +1 -0
  103. package/templates/game/rokit.toml +5 -0
  104. package/templates/game/src/client/init.client.cpp +1 -0
  105. package/templates/game/src/server/leaderstats.server.cpp +1 -0
  106. package/src/api.js +0 -57
  107. package/src/cli.js +0 -481
  108. package/src/compile.js +0 -56
  109. package/src/editor-install.js +0 -218
  110. package/src/intellisense.js +0 -1368
  111. package/src/lsp.js +0 -227
  112. /package/{src → examples/game/src}/shared/config.cpp +0 -0
  113. /package/{src → examples/game/src}/shared/config.h +0 -0
package/src/headers.ts ADDED
@@ -0,0 +1,233 @@
1
+ // @ts-nocheck
2
+ import path from "node:path";
3
+ import { luauType } from "./api.js";
4
+
5
+ function fileStem(fileName) {
6
+ return path.basename(String(fileName || "module")).replace(/\.(h|hpp|hh|cpp|cc|cxx|c)$/i, "");
7
+ }
8
+
9
+ function emitValue(node) {
10
+ if (!node) {
11
+ return "nil";
12
+ }
13
+ if (node.type === "number") {
14
+ return node.value;
15
+ }
16
+ if (node.type === "string") {
17
+ return `"${node.value}"`;
18
+ }
19
+ if (node.type === "bool") {
20
+ return node.value ? "true" : "false";
21
+ }
22
+ if (node.type === "null") {
23
+ return "nil";
24
+ }
25
+ if (node.type === "initlist") {
26
+ const inner = (node.fields || []).map((field) => `${field.name} = ${emitValue(field.value)}`).join(", ");
27
+ return `{ ${inner} }`;
28
+ }
29
+ return "nil";
30
+ }
31
+
32
+ function collectHeaderShape(ast, fileName) {
33
+ const owners = new Map();
34
+ const ensure = (name) => {
35
+ if (!owners.has(name)) {
36
+ owners.set(name, { fields: [], methods: [] });
37
+ }
38
+ return owners.get(name);
39
+ };
40
+ const freeProtos = [];
41
+ const consts = [];
42
+ for (const decl of ast.body || []) {
43
+ if (decl.type === "decl" && decl.owner) {
44
+ ensure(decl.owner).fields.push(decl);
45
+ continue;
46
+ }
47
+ if (decl.type === "decl") {
48
+ consts.push(decl);
49
+ continue;
50
+ }
51
+ if ((decl.type === "proto" || decl.type === "function") && decl.owner) {
52
+ ensure(decl.owner).methods.push(decl);
53
+ continue;
54
+ }
55
+ if (decl.type === "proto") {
56
+ freeProtos.push(decl);
57
+ }
58
+ }
59
+ const nested = new Set();
60
+ for (const [ownerName, spec] of owners) {
61
+ for (const field of spec.fields) {
62
+ if (field.valueType && owners.has(field.valueType) && field.valueType !== ownerName) {
63
+ nested.add(field.valueType);
64
+ }
65
+ }
66
+ }
67
+ const roots = [...owners.keys()].filter((name) => !nested.has(name));
68
+ const withMethods = roots.filter((name) => ensure(name).methods.length > 0);
69
+ const stem = fileStem(fileName);
70
+ let primary = withMethods[0] || (freeProtos.length ? stem : roots[0] || stem);
71
+ return { owners, nested, roots, freeProtos, consts, primary, stem };
72
+ }
73
+
74
+ function primaryHeaderName(ast, fileName) {
75
+ return collectHeaderShape(ast, fileName).primary;
76
+ }
77
+
78
+ function methodType(fn, owner) {
79
+ const params = [];
80
+ if (owner) {
81
+ params.push(`self: ${owner}`);
82
+ }
83
+ for (const param of fn.params || []) {
84
+ if (typeof param === "string") {
85
+ params.push(param);
86
+ continue;
87
+ }
88
+ const typeAnn = luauType(param.valueType);
89
+ params.push(typeAnn ? `${param.name}: ${typeAnn}` : param.name);
90
+ }
91
+ const ret = luauType(fn.returnType);
92
+ const retAnn = !ret || ret === "()" ? "()" : ret;
93
+ return `(${params.join(", ")}) -> ${retAnn}`;
94
+ }
95
+
96
+ function fieldType(field, owners) {
97
+ if (field.valueType && owners.has(field.valueType)) {
98
+ return field.valueType;
99
+ }
100
+ return luauType(field.valueType) || "any";
101
+ }
102
+
103
+ function emitExportType(lines, name, spec, owners, extraMethods, extraFields) {
104
+ lines.push(`export type ${name} = {`);
105
+ const seen = new Set();
106
+ for (const field of [...((spec && spec.fields) || []), ...(extraFields || [])]) {
107
+ if (!field.name || seen.has(field.name)) {
108
+ continue;
109
+ }
110
+ seen.add(field.name);
111
+ lines.push(` ${field.name}: ${fieldType(field, owners)},`);
112
+ }
113
+ for (const method of (spec && spec.methods) || []) {
114
+ if (!method.name || seen.has(method.name)) {
115
+ continue;
116
+ }
117
+ seen.add(method.name);
118
+ lines.push(` ${method.name}: ${methodType(method, name)},`);
119
+ }
120
+ for (const method of extraMethods || []) {
121
+ if (!method.name || seen.has(method.name)) {
122
+ continue;
123
+ }
124
+ seen.add(method.name);
125
+ lines.push(` ${method.name}: ${methodType(method, null)},`);
126
+ }
127
+ lines.push("}");
128
+ lines.push("");
129
+ }
130
+
131
+ function emitDataConstructor(lines, name, spec, owners) {
132
+ const emitField = (field) => {
133
+ if (field.valueType && owners.has(field.valueType)) {
134
+ const nested = owners.get(field.valueType);
135
+ const inner = nested.fields.map((item) => `${item.name} = ${emitValue(item.value)}`).join(", ");
136
+ return `{ ${inner} }`;
137
+ }
138
+ if (field.value && field.value.type === "initlist") {
139
+ return emitValue(field.value);
140
+ }
141
+ return emitValue(field.value);
142
+ };
143
+ lines.push(`const function ${name}(): ${name}`);
144
+ lines.push(" return {");
145
+ for (const field of spec.fields) {
146
+ lines.push(` ${field.name} = ${emitField(field)},`);
147
+ }
148
+ lines.push(" }");
149
+ lines.push("end");
150
+ lines.push("");
151
+ }
152
+
153
+ function emitHeaderModule(ast, options = {}) {
154
+ const strict = options.strict === true;
155
+ const fileName = options.relativeName || options.filePath || "header.h";
156
+ const shape = collectHeaderShape(ast, fileName);
157
+ const lines = [];
158
+ if (strict) {
159
+ lines.push("--!strict");
160
+ }
161
+ lines.push("-- Compiled by Cluaupp — C++ × Luau");
162
+ lines.push("");
163
+
164
+ const typeOrder = [...shape.nested, ...shape.roots];
165
+ for (const name of typeOrder) {
166
+ const extra = name === shape.primary ? shape.freeProtos : [];
167
+ emitExportType(lines, name, shape.owners.get(name), shape.owners, extra);
168
+ }
169
+ if (shape.freeProtos.length && !shape.owners.has(shape.primary)) {
170
+ emitExportType(lines, shape.primary, { fields: [], methods: [] }, shape.owners, shape.freeProtos, shape.consts);
171
+ }
172
+
173
+ for (const decl of shape.consts) {
174
+ const typeAnn = luauType(decl.valueType);
175
+ const typed = typeAnn ? `: ${typeAnn}` : "";
176
+ lines.push(`const ${decl.name}${typed} = ${emitValue(decl.value)}`);
177
+ }
178
+ if (shape.consts.length) {
179
+ lines.push("");
180
+ }
181
+
182
+ const hasMethods = shape.roots.some((name) => shape.owners.get(name).methods.length > 0) || shape.freeProtos.length > 0;
183
+ const implName = hasMethods ? options.headerImplName : null;
184
+
185
+ if (!hasMethods && shape.roots.length > 0) {
186
+ for (const name of shape.roots) {
187
+ emitDataConstructor(lines, name, shape.owners.get(name), shape.owners);
188
+ }
189
+ lines.push(`return ${shape.primary}`);
190
+ lines.push("");
191
+ return lines.join("\n");
192
+ }
193
+
194
+ if (!hasMethods && shape.consts.length > 0) {
195
+ lines.push("return {");
196
+ for (const decl of shape.consts) {
197
+ lines.push(` ${decl.name} = ${decl.name},`);
198
+ }
199
+ lines.push("}");
200
+ lines.push("");
201
+ return lines.join("\n");
202
+ }
203
+
204
+ const tableName = shape.primary;
205
+ const spec = shape.owners.get(tableName) || { fields: [], methods: [] };
206
+ const methods = [...spec.methods, ...shape.freeProtos];
207
+ lines.push(`const ${tableName}: ${tableName} = {`);
208
+ if (implName) {
209
+ for (const field of spec.fields) {
210
+ if (field.valueType && shape.owners.has(field.valueType) && field.valueType !== tableName) {
211
+ continue;
212
+ }
213
+ lines.push(` ${field.name} = ${implName}.${field.name},`);
214
+ }
215
+ for (const method of methods) {
216
+ lines.push(` ${method.name} = ${implName}.${method.name},`);
217
+ }
218
+ }
219
+ for (const decl of shape.consts) {
220
+ lines.push(` ${decl.name} = ${decl.name},`);
221
+ }
222
+ lines.push("}");
223
+ lines.push("");
224
+ lines.push(`return ${tableName}`);
225
+ lines.push("");
226
+ return lines.join("\n");
227
+ }
228
+
229
+ export {
230
+ emitHeaderModule,
231
+ primaryHeaderName,
232
+ collectHeaderShape,
233
+ };
@@ -0,0 +1,286 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { parse } from "./parse.js";
5
+ import { preprocess } from "./preprocess.js";
6
+ import { findClangPlusPlus, installMarketplaceExtension, ensureLlvm, CLANGD_ID, reportInstall } from "./editor-install.js";
7
+ import { pkg, projectRoot as PACKAGE_ROOT } from "./package-info.js";
8
+ import type { ProjectConfig } from "./types.js";
9
+ import type { PreprocessOptions } from "./ast.js";
10
+
11
+ export type CluauppDiagnostic = {
12
+ line: number;
13
+ col: number;
14
+ message: string;
15
+ severity: "error";
16
+ };
17
+
18
+ function posix(file: string): string {
19
+ return String(file).replace(/\\/g, "/");
20
+ }
21
+
22
+ function collectFiles(dir: string, files: string[] = []): string[] {
23
+ if (!fs.existsSync(dir)) {
24
+ return files;
25
+ }
26
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
27
+ const full = path.join(dir, entry.name);
28
+ if (entry.isDirectory()) {
29
+ collectFiles(full, files);
30
+ } else {
31
+ files.push(full);
32
+ }
33
+ }
34
+ return files;
35
+ }
36
+
37
+ function isCppFile(file: string): boolean {
38
+ return /\.(cpp|cc|cxx|c|h|hpp|hh)$/i.test(file);
39
+ }
40
+
41
+ function clangCompileArgs(root: string): string[] {
42
+ const absRoot = posix(path.resolve(root));
43
+ const absInclude = posix(path.join(absRoot, "include"));
44
+ const absSrc = posix(path.join(absRoot, "src"));
45
+ const roblox = posix(path.join(absInclude, "cluaupp", "roblox.hpp"));
46
+ return [
47
+ "-xc++",
48
+ "-std=c++20",
49
+ "-ferror-limit=0",
50
+ "-I" + absInclude,
51
+ "-I" + absSrc,
52
+ "-Iinclude",
53
+ "-Isrc",
54
+ "-include",
55
+ roblox,
56
+ ];
57
+ }
58
+
59
+ function compileCommandEntry(root: string, file: string) {
60
+ const absRoot = posix(path.resolve(root));
61
+ const absFile = posix(path.resolve(file));
62
+ return {
63
+ directory: absRoot,
64
+ file: absFile,
65
+ arguments: [clangPath(), ...clangCompileArgs(root), "-c", absFile],
66
+ };
67
+ }
68
+
69
+ function mergeJson(file: string, patch: Record<string, unknown>): boolean {
70
+ let current: Record<string, unknown> = {};
71
+ if (fs.existsSync(file)) {
72
+ try {
73
+ current = JSON.parse(fs.readFileSync(file, "utf8")) as Record<string, unknown>;
74
+ } catch {
75
+ current = {};
76
+ }
77
+ }
78
+ const next: Record<string, unknown> = { ...current, ...patch };
79
+ for (const key of Object.keys(patch)) {
80
+ const value = patch[key];
81
+ const existing = current[key];
82
+ if (value && typeof value === "object" && !Array.isArray(value) && existing && typeof existing === "object" && !Array.isArray(existing)) {
83
+ next[key] = { ...(existing as Record<string, unknown>), ...(value as Record<string, unknown>) };
84
+ }
85
+ }
86
+ const json = `${JSON.stringify(next, null, "\t")}\n`;
87
+ if (fs.existsSync(file) && fs.readFileSync(file, "utf8") === json) {
88
+ return false;
89
+ }
90
+ fs.mkdirSync(path.dirname(file), { recursive: true });
91
+ fs.writeFileSync(file, json, "utf8");
92
+ return true;
93
+ }
94
+
95
+ function copyDir(from: string, to: string): void {
96
+ fs.mkdirSync(to, { recursive: true });
97
+ for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
98
+ const src = path.join(from, entry.name);
99
+ const dest = path.join(to, entry.name);
100
+ if (entry.isDirectory()) {
101
+ copyDir(src, dest);
102
+ } else {
103
+ fs.copyFileSync(src, dest);
104
+ }
105
+ }
106
+ }
107
+
108
+ function clangdEditorArgs(): string[] {
109
+ return [
110
+ "--compile-commands-dir=${workspaceFolder}",
111
+ "--header-insertion=never",
112
+ "--query-driver=**/clang++*,**/clang++.exe,**/g++*,**/g++.exe",
113
+ ];
114
+ }
115
+
116
+ export function intelliSenseMode(): string {
117
+ const arch = process.arch === "arm64" ? "arm64" : "x64";
118
+ if (process.platform === "win32") {
119
+ return `windows-clang-${arch}`;
120
+ }
121
+ if (process.platform === "darwin") {
122
+ return `macos-clang-${arch}`;
123
+ }
124
+ return `linux-clang-${arch}`;
125
+ }
126
+
127
+ export function clangPath(): string {
128
+ return findClangPlusPlus() || "clang++";
129
+ }
130
+
131
+ export function writeCompileCommands(root: string, srcDir: string): boolean {
132
+ const files = collectFiles(srcDir).filter((file) => isCppFile(file)).sort();
133
+ const json = `${JSON.stringify(files.map((file) => compileCommandEntry(root, file)), null, "\t")}\n`;
134
+ const dest = path.join(root, "compile_commands.json");
135
+ if (fs.existsSync(dest) && fs.readFileSync(dest, "utf8") === json) {
136
+ return false;
137
+ }
138
+ fs.writeFileSync(dest, json, "utf8");
139
+ return true;
140
+ }
141
+
142
+ export function writeClangd(root: string): boolean {
143
+ const flags = clangCompileArgs(root)
144
+ .map((flag) => ` - ${flag}`)
145
+ .join("\n");
146
+ const contents = `CompileFlags:
147
+ CompilationDatabase: .
148
+ Add:
149
+ ${flags}
150
+
151
+ Diagnostics:
152
+ Suppress: '*'
153
+
154
+ ---
155
+ If:
156
+ PathMatch: .*\\.(cpp|cc|cxx|c|h|hpp|hh)$
157
+ CompileFlags:
158
+ Add:
159
+ - -xc++
160
+
161
+ ---
162
+ If:
163
+ PathMatch: (out|libs)/.*
164
+ Index:
165
+ Background: Skip
166
+ `;
167
+ const dest = path.join(root, ".clangd");
168
+ if (fs.existsSync(dest) && fs.readFileSync(dest, "utf8") === contents) {
169
+ return false;
170
+ }
171
+ fs.writeFileSync(dest, contents, "utf8");
172
+ return true;
173
+ }
174
+
175
+ function writeVscode(root: string): void {
176
+ const settings = path.join(root, ".vscode", "settings.json");
177
+ mergeJson(settings, {
178
+ "C_Cpp.intelliSenseEngine": "Disabled",
179
+ "C_Cpp.default.cppStandard": "c++20",
180
+ "C_Cpp.default.cStandard": "c17",
181
+ "C_Cpp.default.compilerPath": clangPath(),
182
+ "C_Cpp.default.intelliSenseMode": intelliSenseMode(),
183
+ "C_Cpp.default.includePath": ["${workspaceFolder}/include", "${workspaceFolder}/src"],
184
+ "clangd.enable": true,
185
+ "clangd.arguments": clangdEditorArgs(),
186
+ "files.associations": {
187
+ "*.hpp": "cpp",
188
+ "*.h": "cpp",
189
+ "*.server.cpp": "cpp",
190
+ "*.client.cpp": "cpp",
191
+ "*.plugin.cpp": "cpp",
192
+ "*.legacy.cpp": "cpp",
193
+ "*.legacy.server.cpp": "cpp",
194
+ "*.legacy.client.cpp": "cpp",
195
+ },
196
+ });
197
+ mergeJson(path.join(root, ".vscode", "extensions.json"), {
198
+ recommendations: ["llvm-vs-code-extensions.vscode-clangd"],
199
+ });
200
+ const props = path.join(root, ".vscode", "c_cpp_properties.json");
201
+ const nextProps = {
202
+ configurations: [
203
+ {
204
+ name: "Cluaupp",
205
+ compilerPath: clangPath(),
206
+ cStandard: "c17",
207
+ cppStandard: "c++20",
208
+ intelliSenseMode: intelliSenseMode(),
209
+ includePath: ["${workspaceFolder}/include", "${workspaceFolder}/src"],
210
+ forcedInclude: ["${workspaceFolder}/include/cluaupp/roblox.hpp"],
211
+ compileCommands: "${workspaceFolder}/compile_commands.json",
212
+ browse: {
213
+ path: ["${workspaceFolder}/include", "${workspaceFolder}/src"],
214
+ limitSymbolsToIncludedHeaders: true,
215
+ },
216
+ },
217
+ ],
218
+ version: 4,
219
+ };
220
+ const json = `${JSON.stringify(nextProps, null, "\t")}\n`;
221
+ fs.mkdirSync(path.dirname(props), { recursive: true });
222
+ if (!fs.existsSync(props) || fs.readFileSync(props, "utf8") !== json) {
223
+ fs.writeFileSync(props, json, "utf8");
224
+ }
225
+ }
226
+
227
+ export function diagnosticsFor(source: string, fileName?: string, options: PreprocessOptions = {}): CluauppDiagnostic[] {
228
+ try {
229
+ const prepared = options.filePath ? preprocess(source, options.filePath, options) : source;
230
+ parse(prepared, fileName || "input.cpp");
231
+ return [];
232
+ } catch (err) {
233
+ const error = err as { message?: string; line?: number; col?: number };
234
+ const message = error.message ? String(error.message) : "parse error";
235
+ const match = message.match(/:(\d+):(\d+):\s*(.*)$/);
236
+ if (match) {
237
+ return [{ line: Number(match[1]), col: Number(match[2]), message: match[3], severity: "error" }];
238
+ }
239
+ return [{ line: error.line || 1, col: error.col || 1, message, severity: "error" }];
240
+ }
241
+ }
242
+
243
+ export function syncEditorSupport(root: string, config: Partial<ProjectConfig> = {}): void {
244
+ const srcDir = path.join(root, config.rootDir || "src");
245
+ if (fs.existsSync(srcDir)) {
246
+ writeCompileCommands(root, srcDir);
247
+ }
248
+ writeClangd(root);
249
+ writeVscode(root);
250
+ const flags = ["-xc++", "-std=c++20", "-ferror-limit=0", "-Iinclude", "-Isrc", "-include", "include/cluaupp/roblox.hpp"].join("\n") + "\n";
251
+ const flagsFile = path.join(root, "compile_flags.txt");
252
+ if (!fs.existsSync(flagsFile) || fs.readFileSync(flagsFile, "utf8") !== flags) {
253
+ fs.writeFileSync(flagsFile, flags, "utf8");
254
+ }
255
+ }
256
+
257
+ export function installEditorExtension(cluauppRoot = PACKAGE_ROOT): string[] {
258
+ const from = path.join(cluauppRoot, "editors", "vscode");
259
+ if (!fs.existsSync(from)) {
260
+ return [];
261
+ }
262
+ const id = `kartzdev.cluaupp-diagnostics-${pkg.version}`;
263
+ const homes = [path.join(os.homedir(), ".cursor", "extensions"), path.join(os.homedir(), ".vscode", "extensions")];
264
+ const installed: string[] = [];
265
+ for (const home of homes) {
266
+ if (!fs.existsSync(path.dirname(home))) {
267
+ continue;
268
+ }
269
+ fs.mkdirSync(home, { recursive: true });
270
+ const dest = path.join(home, id);
271
+ copyDir(from, dest);
272
+ fs.writeFileSync(path.join(dest, "cluaupp.root"), path.resolve(cluauppRoot), "utf8");
273
+ installed.push(dest);
274
+ }
275
+ return installed;
276
+ }
277
+
278
+ export async function installEditorSupport(cluauppRoot = PACKAGE_ROOT) {
279
+ const local = installEditorExtension(cluauppRoot);
280
+ const llvm = ensureLlvm();
281
+ reportInstall(llvm, "LLVM clang++");
282
+ const clangd = await installMarketplaceExtension(CLANGD_ID, "clangd (LLVM)");
283
+ reportInstall(clangd, "clangd");
284
+ console.log("cluaupp: C++ completion is clangd — reload the editor (Ctrl+Shift+P → Developer: Reload Window)");
285
+ return { local, llvm, clangd };
286
+ }