luaut-language-server 1.1.0 → 2.0.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.
- package/dist/{chunk-HK7PKBDB.js → chunk-SXRYJV32.js} +421 -143
- package/dist/chunk-SXRYJV32.js.map +1 -0
- package/dist/cli.cjs +414 -139
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +414 -139
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -16
- package/dist/index.d.ts +50 -16
- package/dist/index.js +1 -1
- package/package.json +4 -2
- package/dist/chunk-HK7PKBDB.js.map +0 -1
package/dist/cli.cjs
CHANGED
|
@@ -9,19 +9,83 @@ var import_vscode_languageserver_textdocument = require("vscode-languageserver-t
|
|
|
9
9
|
var import_node_fs = require("fs");
|
|
10
10
|
var import_node_path = require("path");
|
|
11
11
|
var import_node_url = require("url");
|
|
12
|
+
var import_luaut_parser2 = require("luaut-parser");
|
|
13
|
+
|
|
14
|
+
// src/features/members.ts
|
|
12
15
|
var import_luaut_parser = require("luaut-parser");
|
|
16
|
+
function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
|
|
17
|
+
if (!type || seen.has(type)) return [];
|
|
18
|
+
seen.add(type);
|
|
19
|
+
switch (type.kind) {
|
|
20
|
+
case "object": {
|
|
21
|
+
const out = [];
|
|
22
|
+
for (const [name, property] of type.properties) {
|
|
23
|
+
out.push({ name, property, isMethod: takesSelf(property.type) });
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
case "intersection": {
|
|
28
|
+
const merged = /* @__PURE__ */ new Map();
|
|
29
|
+
for (const part of type.types) {
|
|
30
|
+
for (const member of membersOf(part, aliases, seen)) merged.set(member.name, member);
|
|
31
|
+
}
|
|
32
|
+
return [...merged.values()];
|
|
33
|
+
}
|
|
34
|
+
case "union": {
|
|
35
|
+
const perBranch = type.types.map((part) => membersOf(part, aliases, seen));
|
|
36
|
+
if (!perBranch.length) return [];
|
|
37
|
+
const [first, ...rest] = perBranch;
|
|
38
|
+
return first.filter((member) => rest.every((other) => other.some((m) => m.name === member.name)));
|
|
39
|
+
}
|
|
40
|
+
case "genericRef": {
|
|
41
|
+
const alias = aliases.get(type.name);
|
|
42
|
+
return alias ? membersOf(alias, aliases, seen) : [];
|
|
43
|
+
}
|
|
44
|
+
case "typeParam":
|
|
45
|
+
return membersOf(type.constraint, aliases, seen);
|
|
46
|
+
default:
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function takesSelf(type) {
|
|
51
|
+
for (const signature of signaturesOf(type)) {
|
|
52
|
+
if (signature.params[0]?.name === "self") return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
function signaturesOf(type, aliases) {
|
|
57
|
+
if (!type) return [];
|
|
58
|
+
if (type.kind === "function") return [type];
|
|
59
|
+
if (type.kind === "intersection") return type.types.flatMap((t) => signaturesOf(t, aliases));
|
|
60
|
+
if (type.kind === "genericRef" && aliases) {
|
|
61
|
+
const alias = aliases.get(type.name);
|
|
62
|
+
return alias ? signaturesOf(alias, aliases) : [];
|
|
63
|
+
}
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
function signatureLabel(signature) {
|
|
67
|
+
const parameters = signature.params.map((p, i) => {
|
|
68
|
+
const name = p.name ?? `arg${i + 1}`;
|
|
69
|
+
return `${name}${p.optional ? "?" : ""}: ${(0, import_luaut_parser.formatType)(p.type)}`;
|
|
70
|
+
});
|
|
71
|
+
const generics = signature.typeParams?.length ? `<${signature.typeParams.join(", ")}>` : "";
|
|
72
|
+
const varargs = signature.varargs ? [`...: ${(0, import_luaut_parser.formatType)(signature.varargs)}`] : [];
|
|
73
|
+
const label = `${generics}(${[...parameters, ...varargs].join(", ")}) -> ${(0, import_luaut_parser.formatType)(signature.returns)}`;
|
|
74
|
+
return { label, parameters };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/analysis.ts
|
|
13
78
|
function globalsOf(libs) {
|
|
14
79
|
const names = /* @__PURE__ */ new Set();
|
|
15
|
-
for (const lib of libs)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
for (const statement of statements) {
|
|
20
|
-
if (statement.type === "DeclareStatement") into.add(statement.name);
|
|
80
|
+
for (const lib of libs) {
|
|
81
|
+
for (const statement of lib.body.statements) {
|
|
82
|
+
if (statement.type === "DeclareStatement") names.add(statement.name);
|
|
83
|
+
}
|
|
21
84
|
}
|
|
85
|
+
return [...names];
|
|
22
86
|
}
|
|
23
87
|
function bindingOfNode(analysis, node) {
|
|
24
|
-
const used = (0,
|
|
88
|
+
const used = (0, import_luaut_parser2.getBinding)(analysis.scopes, node);
|
|
25
89
|
if (used) return used;
|
|
26
90
|
return declarationIndex(analysis).get(node);
|
|
27
91
|
}
|
|
@@ -56,20 +120,34 @@ function pathKey(path) {
|
|
|
56
120
|
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
57
121
|
}
|
|
58
122
|
var CYCLE = { values: /* @__PURE__ */ new Map(), types: /* @__PURE__ */ new Map(), partial: true };
|
|
123
|
+
var NO_PROJECT = { project: { fixed: false, problems: [] }, libs: [], globals: [], reads: /* @__PURE__ */ new Map() };
|
|
59
124
|
var Analyzer = class {
|
|
60
|
-
|
|
61
|
-
builtinGlobals;
|
|
125
|
+
fixed;
|
|
62
126
|
openDocument;
|
|
63
127
|
cache = /* @__PURE__ */ new Map();
|
|
64
128
|
/** Imported modules, by path key. */
|
|
65
129
|
modules = /* @__PURE__ */ new Map();
|
|
130
|
+
/** Project contexts, by folder. */
|
|
131
|
+
contexts = /* @__PURE__ */ new Map();
|
|
132
|
+
/** Parsed type libraries, by path — reparsed only when the text changes. */
|
|
133
|
+
libraries = /* @__PURE__ */ new Map();
|
|
134
|
+
/** Sourcemaps turned into types, by path, with what they were built from. */
|
|
135
|
+
sourceMaps = /* @__PURE__ */ new Map();
|
|
136
|
+
/** The analysis run in progress, if any. */
|
|
137
|
+
run;
|
|
66
138
|
constructor(options = {}) {
|
|
67
|
-
this.libs = options.libs ?? import_luaut_parser.defaultLibs;
|
|
68
|
-
this.builtinGlobals = globalsOf(this.libs);
|
|
69
139
|
this.openDocument = options.openDocument;
|
|
140
|
+
if (options.libs) {
|
|
141
|
+
this.fixed = {
|
|
142
|
+
project: { fixed: true, problems: [] },
|
|
143
|
+
libs: options.libs,
|
|
144
|
+
globals: globalsOf(options.libs),
|
|
145
|
+
reads: /* @__PURE__ */ new Map()
|
|
146
|
+
};
|
|
147
|
+
}
|
|
70
148
|
}
|
|
71
149
|
/** Analyze `document`, reusing the previous result while neither it nor
|
|
72
|
-
* anything it
|
|
150
|
+
* anything it read has changed. */
|
|
73
151
|
get(document) {
|
|
74
152
|
const cached = this.cache.get(document.uri);
|
|
75
153
|
const source = document.getText();
|
|
@@ -84,34 +162,41 @@ var Analyzer = class {
|
|
|
84
162
|
* completion, which analyzes a speculatively edited copy of the file. */
|
|
85
163
|
analyze(uri, version, source) {
|
|
86
164
|
const path = pathOfUri(uri);
|
|
87
|
-
return this.analyzeModule(uri, version, source, new Set(
|
|
165
|
+
if (!path) return this.analyzeModule(uri, version, source, /* @__PURE__ */ new Set());
|
|
166
|
+
const key = pathKey(path);
|
|
167
|
+
return this.resolvingCycles(key, () => {
|
|
168
|
+
const analysis = this.analyzeModule(uri, version, source, /* @__PURE__ */ new Set([key]));
|
|
169
|
+
return { result: analysis, exports: () => this.exportsFrom(analysis, /* @__PURE__ */ new Set([key])) };
|
|
170
|
+
});
|
|
88
171
|
}
|
|
89
172
|
forget(uri) {
|
|
90
173
|
this.cache.delete(uri);
|
|
91
174
|
}
|
|
92
|
-
/** The
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
resolveModulePath(fromUri, specifier) {
|
|
96
|
-
return this.moduleCandidates(fromUri, specifier).find((candidate) => this.sourceOf(candidate) !== void 0);
|
|
175
|
+
/** The project a file belongs to. */
|
|
176
|
+
projectOf(uri) {
|
|
177
|
+
return this.contextFor(pathOfUri(uri)).project;
|
|
97
178
|
}
|
|
98
|
-
/**
|
|
99
|
-
|
|
179
|
+
/** The file an import in `fromUri` names: a relative path, or a `paths`
|
|
180
|
+
* alias from the file's config. */
|
|
181
|
+
resolveModulePath(fromUri, specifier) {
|
|
100
182
|
const from = pathOfUri(fromUri);
|
|
101
|
-
if (!from
|
|
102
|
-
|
|
103
|
-
return specifier.endsWith(".luaut") ? [base] : [`${base}.luaut`, `${base}.d.luaut`, (0, import_node_path.join)(base, "index.luaut")];
|
|
183
|
+
if (!from) return void 0;
|
|
184
|
+
return this.candidatesFor(from, specifier).find((candidate) => this.readFile(candidate) !== void 0);
|
|
104
185
|
}
|
|
105
186
|
/** What the module at `path` exports, analyzing it if need be. */
|
|
106
187
|
exportsAt(path) {
|
|
107
|
-
return this.
|
|
188
|
+
return this.resolvingCycles(pathKey(path), () => {
|
|
189
|
+
const exports2 = this.exportsOf(path, /* @__PURE__ */ new Set());
|
|
190
|
+
return { result: exports2, exports: () => exports2 };
|
|
191
|
+
});
|
|
108
192
|
}
|
|
109
193
|
/** The analysis of the module at `path`, analyzing it if need be. */
|
|
110
194
|
moduleAt(path) {
|
|
111
|
-
this.
|
|
195
|
+
this.exportsAt(path);
|
|
112
196
|
return this.modules.get(pathKey(path))?.analysis;
|
|
113
197
|
}
|
|
114
|
-
|
|
198
|
+
/** A file's text: the open document if there is one, else the disk. */
|
|
199
|
+
readFile(path) {
|
|
115
200
|
const open = this.openDocument?.(path);
|
|
116
201
|
if (open) return open.getText();
|
|
117
202
|
try {
|
|
@@ -120,60 +205,206 @@ var Analyzer = class {
|
|
|
120
205
|
return void 0;
|
|
121
206
|
}
|
|
122
207
|
}
|
|
208
|
+
candidatesFor(from, specifier) {
|
|
209
|
+
return (0, import_luaut_parser2.moduleCandidates)(from, specifier, this.contextFor(from).project.config);
|
|
210
|
+
}
|
|
211
|
+
// ---------------------------------------------------------------- projects
|
|
212
|
+
contextFor(path) {
|
|
213
|
+
if (this.fixed) return this.fixed;
|
|
214
|
+
if (!path) return NO_PROJECT;
|
|
215
|
+
const key = pathKey((0, import_node_path.dirname)(path));
|
|
216
|
+
const cached = this.contexts.get(key);
|
|
217
|
+
if (cached && this.unchanged(cached.reads)) return cached;
|
|
218
|
+
const context = this.buildContext(path);
|
|
219
|
+
this.contexts.set(key, context);
|
|
220
|
+
return context;
|
|
221
|
+
}
|
|
222
|
+
buildContext(path) {
|
|
223
|
+
const reads = /* @__PURE__ */ new Map();
|
|
224
|
+
const host = {
|
|
225
|
+
readFile: (file) => {
|
|
226
|
+
const text = this.readFile(file);
|
|
227
|
+
reads.set(file, text);
|
|
228
|
+
return text;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const lookup = (0, import_luaut_parser2.findConfig)(path, host);
|
|
232
|
+
const problems = [...lookup.problems];
|
|
233
|
+
const config = lookup.config;
|
|
234
|
+
if (!config) return { project: { fixed: false, problems }, libs: [], globals: [], reads };
|
|
235
|
+
const libraries = (0, import_luaut_parser2.resolveTypeLibraries)(config, host);
|
|
236
|
+
problems.push(...libraries.problems);
|
|
237
|
+
const libs = [];
|
|
238
|
+
for (const file of libraries.files) {
|
|
239
|
+
const program = this.library(file, host, problems);
|
|
240
|
+
if (program) libs.push(program);
|
|
241
|
+
}
|
|
242
|
+
let sourceMap;
|
|
243
|
+
if (config.sourceMap) {
|
|
244
|
+
const text = host.readFile(config.sourceMap);
|
|
245
|
+
if (text === void 0) {
|
|
246
|
+
problems.push({
|
|
247
|
+
file: config.path,
|
|
248
|
+
message: `Cannot find the sourceMap file ${config.sourceMap}`,
|
|
249
|
+
...optionPosition(config, "sourceMap")
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
const result = this.sourceMap(config.sourceMap, text, libs, libraries.files);
|
|
253
|
+
if (result.problem) problems.push({ file: config.sourceMap, message: result.problem, line: 1, column: 1 });
|
|
254
|
+
sourceMap = result.types;
|
|
255
|
+
if (sourceMap) libs.push(sourceMap.program);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return { project: { config, fixed: false, problems }, libs, globals: globalsOf(libs), sourceMap, reads };
|
|
259
|
+
}
|
|
260
|
+
/** A type library's definitions, parsed once per text. */
|
|
261
|
+
library(file, host, problems) {
|
|
262
|
+
const source = host.readFile(file);
|
|
263
|
+
if (source === void 0) return void 0;
|
|
264
|
+
const key = pathKey(file);
|
|
265
|
+
let entry = this.libraries.get(key);
|
|
266
|
+
if (!entry || entry.source !== source) {
|
|
267
|
+
try {
|
|
268
|
+
entry = { source, program: (0, import_luaut_parser2.parse)(source) };
|
|
269
|
+
} catch (error) {
|
|
270
|
+
const { message, line, column } = error;
|
|
271
|
+
entry = {
|
|
272
|
+
source,
|
|
273
|
+
problem: { file, message: `Syntax error in type library: ${message.replace(/\s*\(\d+:\d+\)$/, "")}`, line, column }
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
this.libraries.set(key, entry);
|
|
277
|
+
}
|
|
278
|
+
if (entry.problem) problems.push(entry.problem);
|
|
279
|
+
return entry.program;
|
|
280
|
+
}
|
|
281
|
+
/** A sourcemap's types, rebuilt only when it or the libraries change. */
|
|
282
|
+
sourceMap(path, text, libs, files) {
|
|
283
|
+
const key = pathKey(path);
|
|
284
|
+
const libraries = files.join("\n");
|
|
285
|
+
const cached = this.sourceMaps.get(key);
|
|
286
|
+
if (cached && cached.text === text && cached.libraries === libraries) return cached.result;
|
|
287
|
+
const aliases = aliasesOf(libs);
|
|
288
|
+
const members = /* @__PURE__ */ new Map();
|
|
289
|
+
const result = (0, import_luaut_parser2.sourceMapTypes)(text, path, {
|
|
290
|
+
classes: new Set(aliases.keys()),
|
|
291
|
+
membersOf: (className) => {
|
|
292
|
+
let names = members.get(className);
|
|
293
|
+
if (!names) {
|
|
294
|
+
names = new Set(membersOf(aliases.get(className), aliases).map((member) => member.name));
|
|
295
|
+
members.set(className, names);
|
|
296
|
+
}
|
|
297
|
+
return names;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
this.sourceMaps.set(key, { text, libraries, result });
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
unchanged(reads) {
|
|
304
|
+
for (const [file, text] of reads) if (this.readFile(file) !== text) return false;
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
// ----------------------------------------------------------------- modules
|
|
308
|
+
/** Run one analysis of `root` and everything it imports; if that met an
|
|
309
|
+
* import cycle, run it once more with the first pass's exports standing
|
|
310
|
+
* in for the `any` the cycle left (see `Run`). A call made while a run is
|
|
311
|
+
* already going is part of that run. */
|
|
312
|
+
resolvingCycles(root, analyzeRoot) {
|
|
313
|
+
if (this.run) return analyzeRoot().result;
|
|
314
|
+
const run = { cycles: /* @__PURE__ */ new Set(), analyzed: /* @__PURE__ */ new Set(), provisional: /* @__PURE__ */ new Map() };
|
|
315
|
+
this.run = run;
|
|
316
|
+
try {
|
|
317
|
+
const first = analyzeRoot();
|
|
318
|
+
if (!run.cycles.size) return first.result;
|
|
319
|
+
for (const key of run.cycles) {
|
|
320
|
+
const exports2 = key === root ? first.exports() : this.modules.get(key)?.exports;
|
|
321
|
+
if (exports2 && !exports2.partial) run.provisional.set(key, exports2);
|
|
322
|
+
}
|
|
323
|
+
for (const key of run.analyzed) this.modules.delete(key);
|
|
324
|
+
return analyzeRoot().result;
|
|
325
|
+
} finally {
|
|
326
|
+
this.run = void 0;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/** A module's exports, from its analysis. */
|
|
330
|
+
exportsFrom(analysis, importing) {
|
|
331
|
+
return (0, import_luaut_parser2.moduleExports)(analysis.program, analysis.scopes, analysis.types, (specifier) => {
|
|
332
|
+
const next = this.resolveModulePath(analysis.uri, specifier);
|
|
333
|
+
return next ? this.exportsOf(next, importing) : void 0;
|
|
334
|
+
});
|
|
335
|
+
}
|
|
123
336
|
/** `importing` holds every module on the current import chain, so an
|
|
124
337
|
* import back into one of them is recognized as a cycle. */
|
|
125
338
|
analyzeModule(uri, version, source, importing) {
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
|
|
339
|
+
const path = pathOfUri(uri);
|
|
340
|
+
const context = this.contextFor(path);
|
|
341
|
+
const script = path ? context.sourceMap?.scriptFor(path) : void 0;
|
|
342
|
+
const libs = script ? [...context.libs, script] : context.libs;
|
|
343
|
+
const globals = script ? [...context.globals, "script"] : context.globals;
|
|
344
|
+
const { program, errors } = (0, import_luaut_parser2.parseWithRecovery)(source);
|
|
345
|
+
const scopes = (0, import_luaut_parser2.analyzeScopes)(program, { builtinGlobals: [...globals] });
|
|
346
|
+
const dependencies = new Map(context.reads);
|
|
347
|
+
const types = (0, import_luaut_parser2.analyzeTypes)(program, scopes, {
|
|
348
|
+
libs,
|
|
131
349
|
resolveModule: (specifier) => {
|
|
132
|
-
|
|
350
|
+
if (!path) return void 0;
|
|
351
|
+
const candidates = this.candidatesFor(path, specifier);
|
|
352
|
+
const target = candidates.find((candidate) => this.readFile(candidate) !== void 0);
|
|
133
353
|
if (!target) {
|
|
134
|
-
for (const candidate of
|
|
354
|
+
for (const candidate of candidates) dependencies.set(candidate, void 0);
|
|
135
355
|
return void 0;
|
|
136
356
|
}
|
|
137
357
|
const exports2 = this.exportsOf(target, importing);
|
|
138
|
-
dependencies.set(target, this.
|
|
358
|
+
dependencies.set(target, this.readFile(target));
|
|
139
359
|
return exports2;
|
|
140
360
|
}
|
|
141
361
|
});
|
|
142
|
-
return { uri, version, source, program, parseErrors: errors, scopes, types, dependencies };
|
|
362
|
+
return { uri, version, source, program, parseErrors: errors, scopes, types, dependencies, project: context.project };
|
|
143
363
|
}
|
|
144
364
|
exportsOf(path, importing) {
|
|
145
365
|
const key = pathKey(path);
|
|
146
|
-
if (importing.has(key))
|
|
147
|
-
|
|
366
|
+
if (importing.has(key)) {
|
|
367
|
+
this.run?.cycles.add(key);
|
|
368
|
+
return this.run?.provisional.get(key) ?? CYCLE;
|
|
369
|
+
}
|
|
370
|
+
const source = this.readFile(path);
|
|
148
371
|
if (source === void 0) return void 0;
|
|
149
372
|
const cached = this.modules.get(key);
|
|
150
373
|
if (cached && cached.analysis.source === source && this.isFresh(cached.analysis)) return cached.exports;
|
|
151
374
|
importing.add(key);
|
|
152
375
|
try {
|
|
153
376
|
const analysis = this.analyzeModule(uriOfPath(path), -1, source, importing);
|
|
154
|
-
const exports2 =
|
|
155
|
-
const next = this.resolveModulePath(analysis.uri, specifier);
|
|
156
|
-
return next ? this.exportsOf(next, importing) : void 0;
|
|
157
|
-
});
|
|
377
|
+
const exports2 = this.exportsFrom(analysis, importing);
|
|
158
378
|
this.modules.set(key, { analysis, exports: exports2 });
|
|
379
|
+
this.run?.analyzed.add(key);
|
|
159
380
|
return exports2;
|
|
160
381
|
} finally {
|
|
161
382
|
importing.delete(key);
|
|
162
383
|
}
|
|
163
384
|
}
|
|
164
|
-
/** Does every
|
|
165
|
-
* still have the text it was analyzed against? */
|
|
385
|
+
/** Does every file `analysis` read — and everything the modules it
|
|
386
|
+
* imported read — still have the text it was analyzed against? */
|
|
166
387
|
isFresh(analysis, seen = /* @__PURE__ */ new Set()) {
|
|
167
388
|
if (seen.has(analysis)) return true;
|
|
168
389
|
seen.add(analysis);
|
|
169
390
|
for (const [path, source] of analysis.dependencies) {
|
|
170
|
-
if (this.
|
|
391
|
+
if (this.readFile(path) !== source) return false;
|
|
171
392
|
const module2 = this.modules.get(pathKey(path));
|
|
172
393
|
if (module2 && !this.isFresh(module2.analysis, seen)) return false;
|
|
173
394
|
}
|
|
174
395
|
return true;
|
|
175
396
|
}
|
|
176
397
|
};
|
|
398
|
+
function aliasesOf(libs) {
|
|
399
|
+
const empty = (0, import_luaut_parser2.parse)("");
|
|
400
|
+
return (0, import_luaut_parser2.analyzeTypes)(empty, (0, import_luaut_parser2.analyzeScopes)(empty, {}), { libs, diagnostics: false }).aliases;
|
|
401
|
+
}
|
|
402
|
+
function optionPosition(config, key) {
|
|
403
|
+
const offset = config.source.indexOf(JSON.stringify(key));
|
|
404
|
+
if (offset < 0) return { line: 1, column: 1 };
|
|
405
|
+
const before = config.source.slice(0, offset);
|
|
406
|
+
return { line: before.split("\n").length, column: offset - before.lastIndexOf("\n") };
|
|
407
|
+
}
|
|
177
408
|
|
|
178
409
|
// src/features/imports.ts
|
|
179
410
|
var import_node_fs2 = require("fs");
|
|
@@ -209,16 +440,16 @@ function containsPosition(node, pos, inclusive = false) {
|
|
|
209
440
|
}
|
|
210
441
|
function children(node) {
|
|
211
442
|
const out = [];
|
|
212
|
-
|
|
443
|
+
collect(node, out);
|
|
213
444
|
return out;
|
|
214
445
|
}
|
|
215
|
-
function
|
|
446
|
+
function collect(container, out) {
|
|
216
447
|
for (const key of Object.keys(container)) {
|
|
217
448
|
if (key === "line" || key === "column") continue;
|
|
218
449
|
const value = container[key];
|
|
219
450
|
for (const item of Array.isArray(value) ? value : [value]) {
|
|
220
451
|
if (isSpanned(item)) out.push(item);
|
|
221
|
-
else if (isSpanlessNode(item))
|
|
452
|
+
else if (isSpanlessNode(item)) collect(item, out);
|
|
222
453
|
}
|
|
223
454
|
}
|
|
224
455
|
}
|
|
@@ -248,69 +479,6 @@ function walk(root, visit, parent) {
|
|
|
248
479
|
for (const child of children(root)) walk(child, visit, root);
|
|
249
480
|
}
|
|
250
481
|
|
|
251
|
-
// src/features/members.ts
|
|
252
|
-
var import_luaut_parser2 = require("luaut-parser");
|
|
253
|
-
function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
|
|
254
|
-
if (!type || seen.has(type)) return [];
|
|
255
|
-
seen.add(type);
|
|
256
|
-
switch (type.kind) {
|
|
257
|
-
case "object": {
|
|
258
|
-
const out = [];
|
|
259
|
-
for (const [name, property] of type.properties) {
|
|
260
|
-
out.push({ name, property, isMethod: takesSelf(property.type) });
|
|
261
|
-
}
|
|
262
|
-
return out;
|
|
263
|
-
}
|
|
264
|
-
case "intersection": {
|
|
265
|
-
const merged = /* @__PURE__ */ new Map();
|
|
266
|
-
for (const part of type.types) {
|
|
267
|
-
for (const member of membersOf(part, aliases, seen)) merged.set(member.name, member);
|
|
268
|
-
}
|
|
269
|
-
return [...merged.values()];
|
|
270
|
-
}
|
|
271
|
-
case "union": {
|
|
272
|
-
const perBranch = type.types.map((part) => membersOf(part, aliases, seen));
|
|
273
|
-
if (!perBranch.length) return [];
|
|
274
|
-
const [first, ...rest] = perBranch;
|
|
275
|
-
return first.filter((member) => rest.every((other) => other.some((m) => m.name === member.name)));
|
|
276
|
-
}
|
|
277
|
-
case "genericRef": {
|
|
278
|
-
const alias = aliases.get(type.name);
|
|
279
|
-
return alias ? membersOf(alias, aliases, seen) : [];
|
|
280
|
-
}
|
|
281
|
-
case "typeParam":
|
|
282
|
-
return membersOf(type.constraint, aliases, seen);
|
|
283
|
-
default:
|
|
284
|
-
return [];
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
function takesSelf(type) {
|
|
288
|
-
for (const signature of signaturesOf(type)) {
|
|
289
|
-
if (signature.params[0]?.name === "self") return true;
|
|
290
|
-
}
|
|
291
|
-
return false;
|
|
292
|
-
}
|
|
293
|
-
function signaturesOf(type, aliases) {
|
|
294
|
-
if (!type) return [];
|
|
295
|
-
if (type.kind === "function") return [type];
|
|
296
|
-
if (type.kind === "intersection") return type.types.flatMap((t) => signaturesOf(t, aliases));
|
|
297
|
-
if (type.kind === "genericRef" && aliases) {
|
|
298
|
-
const alias = aliases.get(type.name);
|
|
299
|
-
return alias ? signaturesOf(alias, aliases) : [];
|
|
300
|
-
}
|
|
301
|
-
return [];
|
|
302
|
-
}
|
|
303
|
-
function signatureLabel(signature) {
|
|
304
|
-
const parameters = signature.params.map((p, i) => {
|
|
305
|
-
const name = p.name ?? `arg${i + 1}`;
|
|
306
|
-
return `${name}${p.optional ? "?" : ""}: ${(0, import_luaut_parser2.formatType)(p.type)}`;
|
|
307
|
-
});
|
|
308
|
-
const generics = signature.typeParams?.length ? `<${signature.typeParams.join(", ")}>` : "";
|
|
309
|
-
const varargs = signature.varargs ? [`...: ${(0, import_luaut_parser2.formatType)(signature.varargs)}`] : [];
|
|
310
|
-
const label = `${generics}(${[...parameters, ...varargs].join(", ")}) -> ${(0, import_luaut_parser2.formatType)(signature.returns)}`;
|
|
311
|
-
return { label, parameters };
|
|
312
|
-
}
|
|
313
|
-
|
|
314
482
|
// src/features/imports.ts
|
|
315
483
|
var SUGGEST_AGAIN = { title: "Suggest", command: "editor.action.triggerSuggest" };
|
|
316
484
|
function importCompletion(analyzer, document, position) {
|
|
@@ -322,7 +490,7 @@ function importCompletion(analyzer, document, position) {
|
|
|
322
490
|
const after = text.slice(cursor, lineEnd);
|
|
323
491
|
if (!/^\s*(?:import|export)\b/.test(before)) return void 0;
|
|
324
492
|
const path = /\bfrom\s*(["'])([^"']*)$/.exec(before);
|
|
325
|
-
if (path) return pathItems(document.uri, position, path[2]);
|
|
493
|
+
if (path) return pathItems(analyzer, document.uri, position, path[2]);
|
|
326
494
|
const braces = /^\s*(import|export)\s+(?:[A-Za-z_][A-Za-z0-9_]*\s*,\s*)?\{[^}]*$/.exec(before);
|
|
327
495
|
if (braces) {
|
|
328
496
|
const module2 = /\}\s*from\s*(["'])([^"']+)\1/.exec(after);
|
|
@@ -331,21 +499,52 @@ function importCompletion(analyzer, document, position) {
|
|
|
331
499
|
}
|
|
332
500
|
return void 0;
|
|
333
501
|
}
|
|
334
|
-
function pathItems(fromUri, position, typed) {
|
|
502
|
+
function pathItems(analyzer, fromUri, position, typed) {
|
|
335
503
|
const from = pathOfUri(fromUri);
|
|
336
504
|
if (!from) return [];
|
|
337
|
-
if (
|
|
338
|
-
const
|
|
339
|
-
return
|
|
505
|
+
if (typed.startsWith("./") || typed.startsWith("../")) {
|
|
506
|
+
const slash = typed.lastIndexOf("/");
|
|
507
|
+
return entryItems((0, import_node_path2.resolve)((0, import_node_path2.dirname)(from), typed.slice(0, slash + 1)), rangeBack(position, typed.length - slash - 1), from);
|
|
508
|
+
}
|
|
509
|
+
const items = /* @__PURE__ */ new Map();
|
|
510
|
+
const whole = rangeBack(position, typed.length);
|
|
511
|
+
const offer = (label, folder) => {
|
|
512
|
+
if (!label.startsWith(typed) || label === typed) return;
|
|
513
|
+
items.set(label, {
|
|
340
514
|
label,
|
|
341
|
-
kind: import_vscode_languageserver.CompletionItemKind.Folder,
|
|
342
|
-
textEdit: { range:
|
|
343
|
-
command: SUGGEST_AGAIN
|
|
344
|
-
})
|
|
515
|
+
kind: folder ? import_vscode_languageserver.CompletionItemKind.Folder : import_vscode_languageserver.CompletionItemKind.File,
|
|
516
|
+
textEdit: { range: whole, newText: label },
|
|
517
|
+
command: folder ? SUGGEST_AGAIN : void 0
|
|
518
|
+
});
|
|
519
|
+
};
|
|
520
|
+
offer("./", true);
|
|
521
|
+
offer("../", true);
|
|
522
|
+
const config = analyzer.projectOf(fromUri).config;
|
|
523
|
+
for (const [pattern, targets] of Object.entries(config?.paths ?? {})) {
|
|
524
|
+
const star = pattern.indexOf("*");
|
|
525
|
+
if (star < 0) {
|
|
526
|
+
offer(pattern, false);
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
const prefix = pattern.slice(0, star);
|
|
530
|
+
if (!typed.startsWith(prefix)) {
|
|
531
|
+
offer(prefix, true);
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
const rest = typed.slice(prefix.length);
|
|
535
|
+
const slash = rest.lastIndexOf("/");
|
|
536
|
+
const range = rangeBack(position, rest.length - slash - 1);
|
|
537
|
+
for (const target of targets) {
|
|
538
|
+
const cut = target.indexOf("*");
|
|
539
|
+
const head = cut < 0 ? target : target.slice(0, cut);
|
|
540
|
+
for (const item of entryItems((0, import_node_path2.resolve)(config.baseUrl, head + rest.slice(0, slash + 1)), range, from)) {
|
|
541
|
+
items.set(item.label, item);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
345
544
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
545
|
+
return [...items.values()];
|
|
546
|
+
}
|
|
547
|
+
function entryItems(directory, range, from) {
|
|
349
548
|
let entries;
|
|
350
549
|
try {
|
|
351
550
|
entries = (0, import_node_fs2.readdirSync)(directory, { withFileTypes: true });
|
|
@@ -823,6 +1022,8 @@ var IDENTIFIER_CHAR = /[A-Za-z0-9_]/;
|
|
|
823
1022
|
function completion(analyzer, document, position) {
|
|
824
1023
|
const inImport = importCompletion(analyzer, document, position);
|
|
825
1024
|
if (inImport) return inImport;
|
|
1025
|
+
const inString = stringCompletion(analyzer, document, position);
|
|
1026
|
+
if (inString) return inString;
|
|
826
1027
|
const source = document.getText();
|
|
827
1028
|
const offset = document.offsetAt(position);
|
|
828
1029
|
let start = offset;
|
|
@@ -863,6 +1064,43 @@ function completion(analyzer, document, position) {
|
|
|
863
1064
|
}
|
|
864
1065
|
return valueItems(first.analysis, at);
|
|
865
1066
|
}
|
|
1067
|
+
function stringCompletion(analyzer, document, position) {
|
|
1068
|
+
const analysis = analyzer.get(document);
|
|
1069
|
+
const path = pathAt(analysis.program, position, false);
|
|
1070
|
+
const literal = [...path].reverse().find((n) => n.type === "StringLiteral");
|
|
1071
|
+
if (!literal) return void 0;
|
|
1072
|
+
const expected = analysis.types.expectedTypeOf.get(literal);
|
|
1073
|
+
const values = stringLiterals(expected, analysis.types.aliases);
|
|
1074
|
+
if (!values.length) return [];
|
|
1075
|
+
const line = literal.line.start - 1;
|
|
1076
|
+
const range = literal.line.start === literal.line.end ? {
|
|
1077
|
+
start: { line, character: literal.column.start },
|
|
1078
|
+
end: { line, character: literal.column.end - 2 }
|
|
1079
|
+
} : void 0;
|
|
1080
|
+
return values.map((value) => ({
|
|
1081
|
+
label: value,
|
|
1082
|
+
kind: import_vscode_languageserver4.CompletionItemKind.Constant,
|
|
1083
|
+
...range ? { textEdit: { range, newText: value } } : {}
|
|
1084
|
+
}));
|
|
1085
|
+
}
|
|
1086
|
+
function stringLiterals(type, aliases, seen = /* @__PURE__ */ new Set()) {
|
|
1087
|
+
if (!type || seen.has(type)) return [];
|
|
1088
|
+
seen.add(type);
|
|
1089
|
+
switch (type.kind) {
|
|
1090
|
+
case "literal":
|
|
1091
|
+
return typeof type.value === "string" ? [type.value] : [];
|
|
1092
|
+
case "union":
|
|
1093
|
+
return [...new Set(type.types.flatMap((t) => stringLiterals(t, aliases, seen)))];
|
|
1094
|
+
case "genericRef": {
|
|
1095
|
+
const alias = aliases.get(type.name);
|
|
1096
|
+
return alias ? stringLiterals(alias, aliases, seen) : [];
|
|
1097
|
+
}
|
|
1098
|
+
case "typeParam":
|
|
1099
|
+
return stringLiterals(type.constraint, aliases, seen);
|
|
1100
|
+
default:
|
|
1101
|
+
return [];
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
866
1104
|
function memberOperator(source, wordStart) {
|
|
867
1105
|
const ch = source[wordStart - 1];
|
|
868
1106
|
if (ch === ":") return source[wordStart - 2] === ":" ? void 0 : ":";
|
|
@@ -1137,7 +1375,7 @@ var TOKEN_TYPES = [
|
|
|
1137
1375
|
"method",
|
|
1138
1376
|
"keyword"
|
|
1139
1377
|
];
|
|
1140
|
-
var TOKEN_MODIFIERS = ["declaration", "readonly", "defaultLibrary"];
|
|
1378
|
+
var TOKEN_MODIFIERS = ["declaration", "readonly", "defaultLibrary", "control"];
|
|
1141
1379
|
var semanticTokensLegend = {
|
|
1142
1380
|
tokenTypes: [...TOKEN_TYPES],
|
|
1143
1381
|
tokenModifiers: [...TOKEN_MODIFIERS]
|
|
@@ -1152,8 +1390,10 @@ var SOFT_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
1152
1390
|
"is",
|
|
1153
1391
|
"asserts",
|
|
1154
1392
|
"satisfies",
|
|
1155
|
-
"typeof"
|
|
1393
|
+
"typeof",
|
|
1394
|
+
"default"
|
|
1156
1395
|
]);
|
|
1396
|
+
var CONTROL_KEYWORDS = /* @__PURE__ */ new Set(["default"]);
|
|
1157
1397
|
var PRIMITIVES3 = /* @__PURE__ */ new Set(["any", "unknown", "never", "nil", "boolean", "number", "string", "thread", "buffer"]);
|
|
1158
1398
|
function semanticTokens(analysis) {
|
|
1159
1399
|
const entries = /* @__PURE__ */ new Map();
|
|
@@ -1179,12 +1419,8 @@ function semanticTokens(analysis) {
|
|
|
1179
1419
|
walk2(analysis.program);
|
|
1180
1420
|
for (const token of tokens) {
|
|
1181
1421
|
const value = token.value;
|
|
1182
|
-
if (typeof value !== "string") continue;
|
|
1183
|
-
|
|
1184
|
-
add(token, value.length, "keyword");
|
|
1185
|
-
} else if (token.type === "Identifier" && SOFT_KEYWORDS.has(value)) {
|
|
1186
|
-
add(token, value.length, "keyword");
|
|
1187
|
-
}
|
|
1422
|
+
if (token.type !== "Identifier" || typeof value !== "string" || !SOFT_KEYWORDS.has(value)) continue;
|
|
1423
|
+
add(token, value.length, "keyword", CONTROL_KEYWORDS.has(value) ? ["control"] : []);
|
|
1188
1424
|
}
|
|
1189
1425
|
return { data: encode([...entries.values()]) };
|
|
1190
1426
|
}
|
|
@@ -1390,17 +1626,56 @@ function createServer(connection, options = {}) {
|
|
|
1390
1626
|
const document = documents.get(p.textDocument.uri);
|
|
1391
1627
|
return document ? semanticTokens(analyzer.get(document)) : { data: [] };
|
|
1392
1628
|
});
|
|
1393
|
-
|
|
1394
|
-
void connection.sendDiagnostics({
|
|
1395
|
-
uri: document.uri,
|
|
1396
|
-
version: document.version,
|
|
1397
|
-
diagnostics: diagnostics(analyzer.get(document))
|
|
1398
|
-
});
|
|
1399
|
-
};
|
|
1400
|
-
documents.onDidOpen((e) => publish(e.document));
|
|
1629
|
+
let configUris = /* @__PURE__ */ new Set();
|
|
1401
1630
|
const publishAll = () => {
|
|
1402
|
-
|
|
1631
|
+
const problems = /* @__PURE__ */ new Map();
|
|
1632
|
+
for (const document of documents.all()) {
|
|
1633
|
+
const analysis = analyzer.get(document);
|
|
1634
|
+
void connection.sendDiagnostics({
|
|
1635
|
+
uri: document.uri,
|
|
1636
|
+
version: document.version,
|
|
1637
|
+
diagnostics: [...diagnostics(analysis), ...projectHint(analysis)]
|
|
1638
|
+
});
|
|
1639
|
+
for (const problem of analysis.project.problems) {
|
|
1640
|
+
const uri = uriOfPath(problem.file);
|
|
1641
|
+
const list = problems.get(uri) ?? [];
|
|
1642
|
+
if (!list.some((p) => p.message === problem.message && p.line === problem.line)) list.push(problem);
|
|
1643
|
+
problems.set(uri, list);
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
for (const [uri, list] of problems) {
|
|
1647
|
+
void connection.sendDiagnostics({ uri, diagnostics: list.map(problemDiagnostic) });
|
|
1648
|
+
}
|
|
1649
|
+
for (const uri of configUris) {
|
|
1650
|
+
if (!problems.has(uri)) void connection.sendDiagnostics({ uri, diagnostics: [] });
|
|
1651
|
+
}
|
|
1652
|
+
configUris = new Set(problems.keys());
|
|
1653
|
+
};
|
|
1654
|
+
const problemDiagnostic = (problem) => {
|
|
1655
|
+
const line = Math.max((problem.line ?? 1) - 1, 0);
|
|
1656
|
+
const character = Math.max((problem.column ?? 1) - 1, 0);
|
|
1657
|
+
const text = analyzer.readFile(problem.file)?.split("\n")[line] ?? "";
|
|
1658
|
+
const end = Math.max(text.replace(/\r$/, "").trimEnd().length, character + 1);
|
|
1659
|
+
return {
|
|
1660
|
+
range: { start: { line, character }, end: { line, character: end } },
|
|
1661
|
+
severity: import_node.DiagnosticSeverity.Error,
|
|
1662
|
+
source: "luaut",
|
|
1663
|
+
code: "config",
|
|
1664
|
+
message: problem.message
|
|
1665
|
+
};
|
|
1666
|
+
};
|
|
1667
|
+
const projectHint = (analysis) => {
|
|
1668
|
+
const { project } = analysis;
|
|
1669
|
+
if (project.fixed || project.config || !pathOfUri(analysis.uri)) return [];
|
|
1670
|
+
return [{
|
|
1671
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
|
|
1672
|
+
severity: import_node.DiagnosticSeverity.Information,
|
|
1673
|
+
source: "luaut",
|
|
1674
|
+
code: "no-config",
|
|
1675
|
+
message: 'No luaut.config.json applies to this file, so no types are loaded \u2014 not even `print`. Add one to this folder or a folder above, such as { "types": ["luau"], "paths": {}, "sourceMap": null }'
|
|
1676
|
+
}];
|
|
1403
1677
|
};
|
|
1678
|
+
documents.onDidOpen(publishAll);
|
|
1404
1679
|
documents.onDidChangeContent(publishAll);
|
|
1405
1680
|
connection.onDidChangeWatchedFiles(publishAll);
|
|
1406
1681
|
documents.onDidClose((e) => {
|