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/index.cjs
CHANGED
|
@@ -64,19 +64,83 @@ var import_vscode_languageserver_textdocument = require("vscode-languageserver-t
|
|
|
64
64
|
var import_node_fs = require("fs");
|
|
65
65
|
var import_node_path = require("path");
|
|
66
66
|
var import_node_url = require("url");
|
|
67
|
+
var import_luaut_parser2 = require("luaut-parser");
|
|
68
|
+
|
|
69
|
+
// src/features/members.ts
|
|
67
70
|
var import_luaut_parser = require("luaut-parser");
|
|
71
|
+
function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
|
|
72
|
+
if (!type || seen.has(type)) return [];
|
|
73
|
+
seen.add(type);
|
|
74
|
+
switch (type.kind) {
|
|
75
|
+
case "object": {
|
|
76
|
+
const out = [];
|
|
77
|
+
for (const [name, property] of type.properties) {
|
|
78
|
+
out.push({ name, property, isMethod: takesSelf(property.type) });
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
case "intersection": {
|
|
83
|
+
const merged = /* @__PURE__ */ new Map();
|
|
84
|
+
for (const part of type.types) {
|
|
85
|
+
for (const member of membersOf(part, aliases, seen)) merged.set(member.name, member);
|
|
86
|
+
}
|
|
87
|
+
return [...merged.values()];
|
|
88
|
+
}
|
|
89
|
+
case "union": {
|
|
90
|
+
const perBranch = type.types.map((part) => membersOf(part, aliases, seen));
|
|
91
|
+
if (!perBranch.length) return [];
|
|
92
|
+
const [first, ...rest] = perBranch;
|
|
93
|
+
return first.filter((member) => rest.every((other) => other.some((m) => m.name === member.name)));
|
|
94
|
+
}
|
|
95
|
+
case "genericRef": {
|
|
96
|
+
const alias = aliases.get(type.name);
|
|
97
|
+
return alias ? membersOf(alias, aliases, seen) : [];
|
|
98
|
+
}
|
|
99
|
+
case "typeParam":
|
|
100
|
+
return membersOf(type.constraint, aliases, seen);
|
|
101
|
+
default:
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function takesSelf(type) {
|
|
106
|
+
for (const signature of signaturesOf(type)) {
|
|
107
|
+
if (signature.params[0]?.name === "self") return true;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
function signaturesOf(type, aliases) {
|
|
112
|
+
if (!type) return [];
|
|
113
|
+
if (type.kind === "function") return [type];
|
|
114
|
+
if (type.kind === "intersection") return type.types.flatMap((t) => signaturesOf(t, aliases));
|
|
115
|
+
if (type.kind === "genericRef" && aliases) {
|
|
116
|
+
const alias = aliases.get(type.name);
|
|
117
|
+
return alias ? signaturesOf(alias, aliases) : [];
|
|
118
|
+
}
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
function signatureLabel(signature) {
|
|
122
|
+
const parameters = signature.params.map((p, i) => {
|
|
123
|
+
const name = p.name ?? `arg${i + 1}`;
|
|
124
|
+
return `${name}${p.optional ? "?" : ""}: ${(0, import_luaut_parser.formatType)(p.type)}`;
|
|
125
|
+
});
|
|
126
|
+
const generics = signature.typeParams?.length ? `<${signature.typeParams.join(", ")}>` : "";
|
|
127
|
+
const varargs = signature.varargs ? [`...: ${(0, import_luaut_parser.formatType)(signature.varargs)}`] : [];
|
|
128
|
+
const label = `${generics}(${[...parameters, ...varargs].join(", ")}) -> ${(0, import_luaut_parser.formatType)(signature.returns)}`;
|
|
129
|
+
return { label, parameters };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/analysis.ts
|
|
68
133
|
function globalsOf(libs) {
|
|
69
134
|
const names = /* @__PURE__ */ new Set();
|
|
70
|
-
for (const lib of libs)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
for (const statement of statements) {
|
|
75
|
-
if (statement.type === "DeclareStatement") into.add(statement.name);
|
|
135
|
+
for (const lib of libs) {
|
|
136
|
+
for (const statement of lib.body.statements) {
|
|
137
|
+
if (statement.type === "DeclareStatement") names.add(statement.name);
|
|
138
|
+
}
|
|
76
139
|
}
|
|
140
|
+
return [...names];
|
|
77
141
|
}
|
|
78
142
|
function bindingOfNode(analysis, node) {
|
|
79
|
-
const used = (0,
|
|
143
|
+
const used = (0, import_luaut_parser2.getBinding)(analysis.scopes, node);
|
|
80
144
|
if (used) return used;
|
|
81
145
|
return declarationIndex(analysis).get(node);
|
|
82
146
|
}
|
|
@@ -111,20 +175,34 @@ function pathKey(path) {
|
|
|
111
175
|
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
112
176
|
}
|
|
113
177
|
var CYCLE = { values: /* @__PURE__ */ new Map(), types: /* @__PURE__ */ new Map(), partial: true };
|
|
178
|
+
var NO_PROJECT = { project: { fixed: false, problems: [] }, libs: [], globals: [], reads: /* @__PURE__ */ new Map() };
|
|
114
179
|
var Analyzer = class {
|
|
115
|
-
|
|
116
|
-
builtinGlobals;
|
|
180
|
+
fixed;
|
|
117
181
|
openDocument;
|
|
118
182
|
cache = /* @__PURE__ */ new Map();
|
|
119
183
|
/** Imported modules, by path key. */
|
|
120
184
|
modules = /* @__PURE__ */ new Map();
|
|
185
|
+
/** Project contexts, by folder. */
|
|
186
|
+
contexts = /* @__PURE__ */ new Map();
|
|
187
|
+
/** Parsed type libraries, by path — reparsed only when the text changes. */
|
|
188
|
+
libraries = /* @__PURE__ */ new Map();
|
|
189
|
+
/** Sourcemaps turned into types, by path, with what they were built from. */
|
|
190
|
+
sourceMaps = /* @__PURE__ */ new Map();
|
|
191
|
+
/** The analysis run in progress, if any. */
|
|
192
|
+
run;
|
|
121
193
|
constructor(options = {}) {
|
|
122
|
-
this.libs = options.libs ?? import_luaut_parser.defaultLibs;
|
|
123
|
-
this.builtinGlobals = globalsOf(this.libs);
|
|
124
194
|
this.openDocument = options.openDocument;
|
|
195
|
+
if (options.libs) {
|
|
196
|
+
this.fixed = {
|
|
197
|
+
project: { fixed: true, problems: [] },
|
|
198
|
+
libs: options.libs,
|
|
199
|
+
globals: globalsOf(options.libs),
|
|
200
|
+
reads: /* @__PURE__ */ new Map()
|
|
201
|
+
};
|
|
202
|
+
}
|
|
125
203
|
}
|
|
126
204
|
/** Analyze `document`, reusing the previous result while neither it nor
|
|
127
|
-
* anything it
|
|
205
|
+
* anything it read has changed. */
|
|
128
206
|
get(document) {
|
|
129
207
|
const cached = this.cache.get(document.uri);
|
|
130
208
|
const source = document.getText();
|
|
@@ -139,34 +217,41 @@ var Analyzer = class {
|
|
|
139
217
|
* completion, which analyzes a speculatively edited copy of the file. */
|
|
140
218
|
analyze(uri, version, source) {
|
|
141
219
|
const path = pathOfUri(uri);
|
|
142
|
-
return this.analyzeModule(uri, version, source, new Set(
|
|
220
|
+
if (!path) return this.analyzeModule(uri, version, source, /* @__PURE__ */ new Set());
|
|
221
|
+
const key = pathKey(path);
|
|
222
|
+
return this.resolvingCycles(key, () => {
|
|
223
|
+
const analysis = this.analyzeModule(uri, version, source, /* @__PURE__ */ new Set([key]));
|
|
224
|
+
return { result: analysis, exports: () => this.exportsFrom(analysis, /* @__PURE__ */ new Set([key])) };
|
|
225
|
+
});
|
|
143
226
|
}
|
|
144
227
|
forget(uri) {
|
|
145
228
|
this.cache.delete(uri);
|
|
146
229
|
}
|
|
147
|
-
/** The
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
resolveModulePath(fromUri, specifier) {
|
|
151
|
-
return this.moduleCandidates(fromUri, specifier).find((candidate) => this.sourceOf(candidate) !== void 0);
|
|
230
|
+
/** The project a file belongs to. */
|
|
231
|
+
projectOf(uri) {
|
|
232
|
+
return this.contextFor(pathOfUri(uri)).project;
|
|
152
233
|
}
|
|
153
|
-
/**
|
|
154
|
-
|
|
234
|
+
/** The file an import in `fromUri` names: a relative path, or a `paths`
|
|
235
|
+
* alias from the file's config. */
|
|
236
|
+
resolveModulePath(fromUri, specifier) {
|
|
155
237
|
const from = pathOfUri(fromUri);
|
|
156
|
-
if (!from
|
|
157
|
-
|
|
158
|
-
return specifier.endsWith(".luaut") ? [base] : [`${base}.luaut`, `${base}.d.luaut`, (0, import_node_path.join)(base, "index.luaut")];
|
|
238
|
+
if (!from) return void 0;
|
|
239
|
+
return this.candidatesFor(from, specifier).find((candidate) => this.readFile(candidate) !== void 0);
|
|
159
240
|
}
|
|
160
241
|
/** What the module at `path` exports, analyzing it if need be. */
|
|
161
242
|
exportsAt(path) {
|
|
162
|
-
return this.
|
|
243
|
+
return this.resolvingCycles(pathKey(path), () => {
|
|
244
|
+
const exports2 = this.exportsOf(path, /* @__PURE__ */ new Set());
|
|
245
|
+
return { result: exports2, exports: () => exports2 };
|
|
246
|
+
});
|
|
163
247
|
}
|
|
164
248
|
/** The analysis of the module at `path`, analyzing it if need be. */
|
|
165
249
|
moduleAt(path) {
|
|
166
|
-
this.
|
|
250
|
+
this.exportsAt(path);
|
|
167
251
|
return this.modules.get(pathKey(path))?.analysis;
|
|
168
252
|
}
|
|
169
|
-
|
|
253
|
+
/** A file's text: the open document if there is one, else the disk. */
|
|
254
|
+
readFile(path) {
|
|
170
255
|
const open = this.openDocument?.(path);
|
|
171
256
|
if (open) return open.getText();
|
|
172
257
|
try {
|
|
@@ -175,60 +260,206 @@ var Analyzer = class {
|
|
|
175
260
|
return void 0;
|
|
176
261
|
}
|
|
177
262
|
}
|
|
263
|
+
candidatesFor(from, specifier) {
|
|
264
|
+
return (0, import_luaut_parser2.moduleCandidates)(from, specifier, this.contextFor(from).project.config);
|
|
265
|
+
}
|
|
266
|
+
// ---------------------------------------------------------------- projects
|
|
267
|
+
contextFor(path) {
|
|
268
|
+
if (this.fixed) return this.fixed;
|
|
269
|
+
if (!path) return NO_PROJECT;
|
|
270
|
+
const key = pathKey((0, import_node_path.dirname)(path));
|
|
271
|
+
const cached = this.contexts.get(key);
|
|
272
|
+
if (cached && this.unchanged(cached.reads)) return cached;
|
|
273
|
+
const context = this.buildContext(path);
|
|
274
|
+
this.contexts.set(key, context);
|
|
275
|
+
return context;
|
|
276
|
+
}
|
|
277
|
+
buildContext(path) {
|
|
278
|
+
const reads = /* @__PURE__ */ new Map();
|
|
279
|
+
const host = {
|
|
280
|
+
readFile: (file) => {
|
|
281
|
+
const text = this.readFile(file);
|
|
282
|
+
reads.set(file, text);
|
|
283
|
+
return text;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const lookup = (0, import_luaut_parser2.findConfig)(path, host);
|
|
287
|
+
const problems = [...lookup.problems];
|
|
288
|
+
const config = lookup.config;
|
|
289
|
+
if (!config) return { project: { fixed: false, problems }, libs: [], globals: [], reads };
|
|
290
|
+
const libraries = (0, import_luaut_parser2.resolveTypeLibraries)(config, host);
|
|
291
|
+
problems.push(...libraries.problems);
|
|
292
|
+
const libs = [];
|
|
293
|
+
for (const file of libraries.files) {
|
|
294
|
+
const program = this.library(file, host, problems);
|
|
295
|
+
if (program) libs.push(program);
|
|
296
|
+
}
|
|
297
|
+
let sourceMap;
|
|
298
|
+
if (config.sourceMap) {
|
|
299
|
+
const text = host.readFile(config.sourceMap);
|
|
300
|
+
if (text === void 0) {
|
|
301
|
+
problems.push({
|
|
302
|
+
file: config.path,
|
|
303
|
+
message: `Cannot find the sourceMap file ${config.sourceMap}`,
|
|
304
|
+
...optionPosition(config, "sourceMap")
|
|
305
|
+
});
|
|
306
|
+
} else {
|
|
307
|
+
const result = this.sourceMap(config.sourceMap, text, libs, libraries.files);
|
|
308
|
+
if (result.problem) problems.push({ file: config.sourceMap, message: result.problem, line: 1, column: 1 });
|
|
309
|
+
sourceMap = result.types;
|
|
310
|
+
if (sourceMap) libs.push(sourceMap.program);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return { project: { config, fixed: false, problems }, libs, globals: globalsOf(libs), sourceMap, reads };
|
|
314
|
+
}
|
|
315
|
+
/** A type library's definitions, parsed once per text. */
|
|
316
|
+
library(file, host, problems) {
|
|
317
|
+
const source = host.readFile(file);
|
|
318
|
+
if (source === void 0) return void 0;
|
|
319
|
+
const key = pathKey(file);
|
|
320
|
+
let entry = this.libraries.get(key);
|
|
321
|
+
if (!entry || entry.source !== source) {
|
|
322
|
+
try {
|
|
323
|
+
entry = { source, program: (0, import_luaut_parser2.parse)(source) };
|
|
324
|
+
} catch (error) {
|
|
325
|
+
const { message, line, column } = error;
|
|
326
|
+
entry = {
|
|
327
|
+
source,
|
|
328
|
+
problem: { file, message: `Syntax error in type library: ${message.replace(/\s*\(\d+:\d+\)$/, "")}`, line, column }
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
this.libraries.set(key, entry);
|
|
332
|
+
}
|
|
333
|
+
if (entry.problem) problems.push(entry.problem);
|
|
334
|
+
return entry.program;
|
|
335
|
+
}
|
|
336
|
+
/** A sourcemap's types, rebuilt only when it or the libraries change. */
|
|
337
|
+
sourceMap(path, text, libs, files) {
|
|
338
|
+
const key = pathKey(path);
|
|
339
|
+
const libraries = files.join("\n");
|
|
340
|
+
const cached = this.sourceMaps.get(key);
|
|
341
|
+
if (cached && cached.text === text && cached.libraries === libraries) return cached.result;
|
|
342
|
+
const aliases = aliasesOf(libs);
|
|
343
|
+
const members = /* @__PURE__ */ new Map();
|
|
344
|
+
const result = (0, import_luaut_parser2.sourceMapTypes)(text, path, {
|
|
345
|
+
classes: new Set(aliases.keys()),
|
|
346
|
+
membersOf: (className) => {
|
|
347
|
+
let names = members.get(className);
|
|
348
|
+
if (!names) {
|
|
349
|
+
names = new Set(membersOf(aliases.get(className), aliases).map((member) => member.name));
|
|
350
|
+
members.set(className, names);
|
|
351
|
+
}
|
|
352
|
+
return names;
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
this.sourceMaps.set(key, { text, libraries, result });
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
unchanged(reads) {
|
|
359
|
+
for (const [file, text] of reads) if (this.readFile(file) !== text) return false;
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
// ----------------------------------------------------------------- modules
|
|
363
|
+
/** Run one analysis of `root` and everything it imports; if that met an
|
|
364
|
+
* import cycle, run it once more with the first pass's exports standing
|
|
365
|
+
* in for the `any` the cycle left (see `Run`). A call made while a run is
|
|
366
|
+
* already going is part of that run. */
|
|
367
|
+
resolvingCycles(root, analyzeRoot) {
|
|
368
|
+
if (this.run) return analyzeRoot().result;
|
|
369
|
+
const run = { cycles: /* @__PURE__ */ new Set(), analyzed: /* @__PURE__ */ new Set(), provisional: /* @__PURE__ */ new Map() };
|
|
370
|
+
this.run = run;
|
|
371
|
+
try {
|
|
372
|
+
const first = analyzeRoot();
|
|
373
|
+
if (!run.cycles.size) return first.result;
|
|
374
|
+
for (const key of run.cycles) {
|
|
375
|
+
const exports2 = key === root ? first.exports() : this.modules.get(key)?.exports;
|
|
376
|
+
if (exports2 && !exports2.partial) run.provisional.set(key, exports2);
|
|
377
|
+
}
|
|
378
|
+
for (const key of run.analyzed) this.modules.delete(key);
|
|
379
|
+
return analyzeRoot().result;
|
|
380
|
+
} finally {
|
|
381
|
+
this.run = void 0;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/** A module's exports, from its analysis. */
|
|
385
|
+
exportsFrom(analysis, importing) {
|
|
386
|
+
return (0, import_luaut_parser2.moduleExports)(analysis.program, analysis.scopes, analysis.types, (specifier) => {
|
|
387
|
+
const next = this.resolveModulePath(analysis.uri, specifier);
|
|
388
|
+
return next ? this.exportsOf(next, importing) : void 0;
|
|
389
|
+
});
|
|
390
|
+
}
|
|
178
391
|
/** `importing` holds every module on the current import chain, so an
|
|
179
392
|
* import back into one of them is recognized as a cycle. */
|
|
180
393
|
analyzeModule(uri, version, source, importing) {
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
|
|
394
|
+
const path = pathOfUri(uri);
|
|
395
|
+
const context = this.contextFor(path);
|
|
396
|
+
const script = path ? context.sourceMap?.scriptFor(path) : void 0;
|
|
397
|
+
const libs = script ? [...context.libs, script] : context.libs;
|
|
398
|
+
const globals = script ? [...context.globals, "script"] : context.globals;
|
|
399
|
+
const { program, errors } = (0, import_luaut_parser2.parseWithRecovery)(source);
|
|
400
|
+
const scopes = (0, import_luaut_parser2.analyzeScopes)(program, { builtinGlobals: [...globals] });
|
|
401
|
+
const dependencies = new Map(context.reads);
|
|
402
|
+
const types = (0, import_luaut_parser2.analyzeTypes)(program, scopes, {
|
|
403
|
+
libs,
|
|
186
404
|
resolveModule: (specifier) => {
|
|
187
|
-
|
|
405
|
+
if (!path) return void 0;
|
|
406
|
+
const candidates = this.candidatesFor(path, specifier);
|
|
407
|
+
const target = candidates.find((candidate) => this.readFile(candidate) !== void 0);
|
|
188
408
|
if (!target) {
|
|
189
|
-
for (const candidate of
|
|
409
|
+
for (const candidate of candidates) dependencies.set(candidate, void 0);
|
|
190
410
|
return void 0;
|
|
191
411
|
}
|
|
192
412
|
const exports2 = this.exportsOf(target, importing);
|
|
193
|
-
dependencies.set(target, this.
|
|
413
|
+
dependencies.set(target, this.readFile(target));
|
|
194
414
|
return exports2;
|
|
195
415
|
}
|
|
196
416
|
});
|
|
197
|
-
return { uri, version, source, program, parseErrors: errors, scopes, types, dependencies };
|
|
417
|
+
return { uri, version, source, program, parseErrors: errors, scopes, types, dependencies, project: context.project };
|
|
198
418
|
}
|
|
199
419
|
exportsOf(path, importing) {
|
|
200
420
|
const key = pathKey(path);
|
|
201
|
-
if (importing.has(key))
|
|
202
|
-
|
|
421
|
+
if (importing.has(key)) {
|
|
422
|
+
this.run?.cycles.add(key);
|
|
423
|
+
return this.run?.provisional.get(key) ?? CYCLE;
|
|
424
|
+
}
|
|
425
|
+
const source = this.readFile(path);
|
|
203
426
|
if (source === void 0) return void 0;
|
|
204
427
|
const cached = this.modules.get(key);
|
|
205
428
|
if (cached && cached.analysis.source === source && this.isFresh(cached.analysis)) return cached.exports;
|
|
206
429
|
importing.add(key);
|
|
207
430
|
try {
|
|
208
431
|
const analysis = this.analyzeModule(uriOfPath(path), -1, source, importing);
|
|
209
|
-
const exports2 =
|
|
210
|
-
const next = this.resolveModulePath(analysis.uri, specifier);
|
|
211
|
-
return next ? this.exportsOf(next, importing) : void 0;
|
|
212
|
-
});
|
|
432
|
+
const exports2 = this.exportsFrom(analysis, importing);
|
|
213
433
|
this.modules.set(key, { analysis, exports: exports2 });
|
|
434
|
+
this.run?.analyzed.add(key);
|
|
214
435
|
return exports2;
|
|
215
436
|
} finally {
|
|
216
437
|
importing.delete(key);
|
|
217
438
|
}
|
|
218
439
|
}
|
|
219
|
-
/** Does every
|
|
220
|
-
* still have the text it was analyzed against? */
|
|
440
|
+
/** Does every file `analysis` read — and everything the modules it
|
|
441
|
+
* imported read — still have the text it was analyzed against? */
|
|
221
442
|
isFresh(analysis, seen = /* @__PURE__ */ new Set()) {
|
|
222
443
|
if (seen.has(analysis)) return true;
|
|
223
444
|
seen.add(analysis);
|
|
224
445
|
for (const [path, source] of analysis.dependencies) {
|
|
225
|
-
if (this.
|
|
446
|
+
if (this.readFile(path) !== source) return false;
|
|
226
447
|
const module2 = this.modules.get(pathKey(path));
|
|
227
448
|
if (module2 && !this.isFresh(module2.analysis, seen)) return false;
|
|
228
449
|
}
|
|
229
450
|
return true;
|
|
230
451
|
}
|
|
231
452
|
};
|
|
453
|
+
function aliasesOf(libs) {
|
|
454
|
+
const empty = (0, import_luaut_parser2.parse)("");
|
|
455
|
+
return (0, import_luaut_parser2.analyzeTypes)(empty, (0, import_luaut_parser2.analyzeScopes)(empty, {}), { libs, diagnostics: false }).aliases;
|
|
456
|
+
}
|
|
457
|
+
function optionPosition(config, key) {
|
|
458
|
+
const offset = config.source.indexOf(JSON.stringify(key));
|
|
459
|
+
if (offset < 0) return { line: 1, column: 1 };
|
|
460
|
+
const before = config.source.slice(0, offset);
|
|
461
|
+
return { line: before.split("\n").length, column: offset - before.lastIndexOf("\n") };
|
|
462
|
+
}
|
|
232
463
|
|
|
233
464
|
// src/features/imports.ts
|
|
234
465
|
var import_node_fs2 = require("fs");
|
|
@@ -264,16 +495,16 @@ function containsPosition(node, pos, inclusive = false) {
|
|
|
264
495
|
}
|
|
265
496
|
function children(node) {
|
|
266
497
|
const out = [];
|
|
267
|
-
|
|
498
|
+
collect(node, out);
|
|
268
499
|
return out;
|
|
269
500
|
}
|
|
270
|
-
function
|
|
501
|
+
function collect(container, out) {
|
|
271
502
|
for (const key of Object.keys(container)) {
|
|
272
503
|
if (key === "line" || key === "column") continue;
|
|
273
504
|
const value = container[key];
|
|
274
505
|
for (const item of Array.isArray(value) ? value : [value]) {
|
|
275
506
|
if (isSpanned(item)) out.push(item);
|
|
276
|
-
else if (isSpanlessNode(item))
|
|
507
|
+
else if (isSpanlessNode(item)) collect(item, out);
|
|
277
508
|
}
|
|
278
509
|
}
|
|
279
510
|
}
|
|
@@ -314,69 +545,6 @@ function walk(root, visit, parent) {
|
|
|
314
545
|
for (const child of children(root)) walk(child, visit, root);
|
|
315
546
|
}
|
|
316
547
|
|
|
317
|
-
// src/features/members.ts
|
|
318
|
-
var import_luaut_parser2 = require("luaut-parser");
|
|
319
|
-
function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
|
|
320
|
-
if (!type || seen.has(type)) return [];
|
|
321
|
-
seen.add(type);
|
|
322
|
-
switch (type.kind) {
|
|
323
|
-
case "object": {
|
|
324
|
-
const out = [];
|
|
325
|
-
for (const [name, property] of type.properties) {
|
|
326
|
-
out.push({ name, property, isMethod: takesSelf(property.type) });
|
|
327
|
-
}
|
|
328
|
-
return out;
|
|
329
|
-
}
|
|
330
|
-
case "intersection": {
|
|
331
|
-
const merged = /* @__PURE__ */ new Map();
|
|
332
|
-
for (const part of type.types) {
|
|
333
|
-
for (const member of membersOf(part, aliases, seen)) merged.set(member.name, member);
|
|
334
|
-
}
|
|
335
|
-
return [...merged.values()];
|
|
336
|
-
}
|
|
337
|
-
case "union": {
|
|
338
|
-
const perBranch = type.types.map((part) => membersOf(part, aliases, seen));
|
|
339
|
-
if (!perBranch.length) return [];
|
|
340
|
-
const [first, ...rest] = perBranch;
|
|
341
|
-
return first.filter((member) => rest.every((other) => other.some((m) => m.name === member.name)));
|
|
342
|
-
}
|
|
343
|
-
case "genericRef": {
|
|
344
|
-
const alias = aliases.get(type.name);
|
|
345
|
-
return alias ? membersOf(alias, aliases, seen) : [];
|
|
346
|
-
}
|
|
347
|
-
case "typeParam":
|
|
348
|
-
return membersOf(type.constraint, aliases, seen);
|
|
349
|
-
default:
|
|
350
|
-
return [];
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
function takesSelf(type) {
|
|
354
|
-
for (const signature of signaturesOf(type)) {
|
|
355
|
-
if (signature.params[0]?.name === "self") return true;
|
|
356
|
-
}
|
|
357
|
-
return false;
|
|
358
|
-
}
|
|
359
|
-
function signaturesOf(type, aliases) {
|
|
360
|
-
if (!type) return [];
|
|
361
|
-
if (type.kind === "function") return [type];
|
|
362
|
-
if (type.kind === "intersection") return type.types.flatMap((t) => signaturesOf(t, aliases));
|
|
363
|
-
if (type.kind === "genericRef" && aliases) {
|
|
364
|
-
const alias = aliases.get(type.name);
|
|
365
|
-
return alias ? signaturesOf(alias, aliases) : [];
|
|
366
|
-
}
|
|
367
|
-
return [];
|
|
368
|
-
}
|
|
369
|
-
function signatureLabel(signature) {
|
|
370
|
-
const parameters = signature.params.map((p, i) => {
|
|
371
|
-
const name = p.name ?? `arg${i + 1}`;
|
|
372
|
-
return `${name}${p.optional ? "?" : ""}: ${(0, import_luaut_parser2.formatType)(p.type)}`;
|
|
373
|
-
});
|
|
374
|
-
const generics = signature.typeParams?.length ? `<${signature.typeParams.join(", ")}>` : "";
|
|
375
|
-
const varargs = signature.varargs ? [`...: ${(0, import_luaut_parser2.formatType)(signature.varargs)}`] : [];
|
|
376
|
-
const label = `${generics}(${[...parameters, ...varargs].join(", ")}) -> ${(0, import_luaut_parser2.formatType)(signature.returns)}`;
|
|
377
|
-
return { label, parameters };
|
|
378
|
-
}
|
|
379
|
-
|
|
380
548
|
// src/features/imports.ts
|
|
381
549
|
var SUGGEST_AGAIN = { title: "Suggest", command: "editor.action.triggerSuggest" };
|
|
382
550
|
function importCompletion(analyzer, document, position) {
|
|
@@ -388,7 +556,7 @@ function importCompletion(analyzer, document, position) {
|
|
|
388
556
|
const after = text.slice(cursor, lineEnd);
|
|
389
557
|
if (!/^\s*(?:import|export)\b/.test(before)) return void 0;
|
|
390
558
|
const path = /\bfrom\s*(["'])([^"']*)$/.exec(before);
|
|
391
|
-
if (path) return pathItems(document.uri, position, path[2]);
|
|
559
|
+
if (path) return pathItems(analyzer, document.uri, position, path[2]);
|
|
392
560
|
const braces = /^\s*(import|export)\s+(?:[A-Za-z_][A-Za-z0-9_]*\s*,\s*)?\{[^}]*$/.exec(before);
|
|
393
561
|
if (braces) {
|
|
394
562
|
const module2 = /\}\s*from\s*(["'])([^"']+)\1/.exec(after);
|
|
@@ -397,21 +565,52 @@ function importCompletion(analyzer, document, position) {
|
|
|
397
565
|
}
|
|
398
566
|
return void 0;
|
|
399
567
|
}
|
|
400
|
-
function pathItems(fromUri, position, typed) {
|
|
568
|
+
function pathItems(analyzer, fromUri, position, typed) {
|
|
401
569
|
const from = pathOfUri(fromUri);
|
|
402
570
|
if (!from) return [];
|
|
403
|
-
if (
|
|
404
|
-
const
|
|
405
|
-
return
|
|
571
|
+
if (typed.startsWith("./") || typed.startsWith("../")) {
|
|
572
|
+
const slash = typed.lastIndexOf("/");
|
|
573
|
+
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);
|
|
574
|
+
}
|
|
575
|
+
const items = /* @__PURE__ */ new Map();
|
|
576
|
+
const whole = rangeBack(position, typed.length);
|
|
577
|
+
const offer = (label, folder) => {
|
|
578
|
+
if (!label.startsWith(typed) || label === typed) return;
|
|
579
|
+
items.set(label, {
|
|
406
580
|
label,
|
|
407
|
-
kind: import_vscode_languageserver.CompletionItemKind.Folder,
|
|
408
|
-
textEdit: { range:
|
|
409
|
-
command: SUGGEST_AGAIN
|
|
410
|
-
})
|
|
581
|
+
kind: folder ? import_vscode_languageserver.CompletionItemKind.Folder : import_vscode_languageserver.CompletionItemKind.File,
|
|
582
|
+
textEdit: { range: whole, newText: label },
|
|
583
|
+
command: folder ? SUGGEST_AGAIN : void 0
|
|
584
|
+
});
|
|
585
|
+
};
|
|
586
|
+
offer("./", true);
|
|
587
|
+
offer("../", true);
|
|
588
|
+
const config = analyzer.projectOf(fromUri).config;
|
|
589
|
+
for (const [pattern, targets] of Object.entries(config?.paths ?? {})) {
|
|
590
|
+
const star = pattern.indexOf("*");
|
|
591
|
+
if (star < 0) {
|
|
592
|
+
offer(pattern, false);
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
const prefix = pattern.slice(0, star);
|
|
596
|
+
if (!typed.startsWith(prefix)) {
|
|
597
|
+
offer(prefix, true);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
const rest = typed.slice(prefix.length);
|
|
601
|
+
const slash = rest.lastIndexOf("/");
|
|
602
|
+
const range = rangeBack(position, rest.length - slash - 1);
|
|
603
|
+
for (const target of targets) {
|
|
604
|
+
const cut = target.indexOf("*");
|
|
605
|
+
const head = cut < 0 ? target : target.slice(0, cut);
|
|
606
|
+
for (const item of entryItems((0, import_node_path2.resolve)(config.baseUrl, head + rest.slice(0, slash + 1)), range, from)) {
|
|
607
|
+
items.set(item.label, item);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
411
610
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
611
|
+
return [...items.values()];
|
|
612
|
+
}
|
|
613
|
+
function entryItems(directory, range, from) {
|
|
415
614
|
let entries;
|
|
416
615
|
try {
|
|
417
616
|
entries = (0, import_node_fs2.readdirSync)(directory, { withFileTypes: true });
|
|
@@ -889,6 +1088,8 @@ var IDENTIFIER_CHAR = /[A-Za-z0-9_]/;
|
|
|
889
1088
|
function completion(analyzer, document, position) {
|
|
890
1089
|
const inImport = importCompletion(analyzer, document, position);
|
|
891
1090
|
if (inImport) return inImport;
|
|
1091
|
+
const inString = stringCompletion(analyzer, document, position);
|
|
1092
|
+
if (inString) return inString;
|
|
892
1093
|
const source = document.getText();
|
|
893
1094
|
const offset = document.offsetAt(position);
|
|
894
1095
|
let start = offset;
|
|
@@ -929,6 +1130,43 @@ function completion(analyzer, document, position) {
|
|
|
929
1130
|
}
|
|
930
1131
|
return valueItems(first.analysis, at);
|
|
931
1132
|
}
|
|
1133
|
+
function stringCompletion(analyzer, document, position) {
|
|
1134
|
+
const analysis = analyzer.get(document);
|
|
1135
|
+
const path = pathAt(analysis.program, position, false);
|
|
1136
|
+
const literal = [...path].reverse().find((n) => n.type === "StringLiteral");
|
|
1137
|
+
if (!literal) return void 0;
|
|
1138
|
+
const expected = analysis.types.expectedTypeOf.get(literal);
|
|
1139
|
+
const values = stringLiterals(expected, analysis.types.aliases);
|
|
1140
|
+
if (!values.length) return [];
|
|
1141
|
+
const line = literal.line.start - 1;
|
|
1142
|
+
const range = literal.line.start === literal.line.end ? {
|
|
1143
|
+
start: { line, character: literal.column.start },
|
|
1144
|
+
end: { line, character: literal.column.end - 2 }
|
|
1145
|
+
} : void 0;
|
|
1146
|
+
return values.map((value) => ({
|
|
1147
|
+
label: value,
|
|
1148
|
+
kind: import_vscode_languageserver4.CompletionItemKind.Constant,
|
|
1149
|
+
...range ? { textEdit: { range, newText: value } } : {}
|
|
1150
|
+
}));
|
|
1151
|
+
}
|
|
1152
|
+
function stringLiterals(type, aliases, seen = /* @__PURE__ */ new Set()) {
|
|
1153
|
+
if (!type || seen.has(type)) return [];
|
|
1154
|
+
seen.add(type);
|
|
1155
|
+
switch (type.kind) {
|
|
1156
|
+
case "literal":
|
|
1157
|
+
return typeof type.value === "string" ? [type.value] : [];
|
|
1158
|
+
case "union":
|
|
1159
|
+
return [...new Set(type.types.flatMap((t) => stringLiterals(t, aliases, seen)))];
|
|
1160
|
+
case "genericRef": {
|
|
1161
|
+
const alias = aliases.get(type.name);
|
|
1162
|
+
return alias ? stringLiterals(alias, aliases, seen) : [];
|
|
1163
|
+
}
|
|
1164
|
+
case "typeParam":
|
|
1165
|
+
return stringLiterals(type.constraint, aliases, seen);
|
|
1166
|
+
default:
|
|
1167
|
+
return [];
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
932
1170
|
function memberOperator(source, wordStart) {
|
|
933
1171
|
const ch = source[wordStart - 1];
|
|
934
1172
|
if (ch === ":") return source[wordStart - 2] === ":" ? void 0 : ":";
|
|
@@ -1203,7 +1441,7 @@ var TOKEN_TYPES = [
|
|
|
1203
1441
|
"method",
|
|
1204
1442
|
"keyword"
|
|
1205
1443
|
];
|
|
1206
|
-
var TOKEN_MODIFIERS = ["declaration", "readonly", "defaultLibrary"];
|
|
1444
|
+
var TOKEN_MODIFIERS = ["declaration", "readonly", "defaultLibrary", "control"];
|
|
1207
1445
|
var semanticTokensLegend = {
|
|
1208
1446
|
tokenTypes: [...TOKEN_TYPES],
|
|
1209
1447
|
tokenModifiers: [...TOKEN_MODIFIERS]
|
|
@@ -1218,8 +1456,10 @@ var SOFT_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
1218
1456
|
"is",
|
|
1219
1457
|
"asserts",
|
|
1220
1458
|
"satisfies",
|
|
1221
|
-
"typeof"
|
|
1459
|
+
"typeof",
|
|
1460
|
+
"default"
|
|
1222
1461
|
]);
|
|
1462
|
+
var CONTROL_KEYWORDS = /* @__PURE__ */ new Set(["default"]);
|
|
1223
1463
|
var PRIMITIVES3 = /* @__PURE__ */ new Set(["any", "unknown", "never", "nil", "boolean", "number", "string", "thread", "buffer"]);
|
|
1224
1464
|
function semanticTokens(analysis) {
|
|
1225
1465
|
const entries = /* @__PURE__ */ new Map();
|
|
@@ -1245,12 +1485,8 @@ function semanticTokens(analysis) {
|
|
|
1245
1485
|
walk2(analysis.program);
|
|
1246
1486
|
for (const token of tokens) {
|
|
1247
1487
|
const value = token.value;
|
|
1248
|
-
if (typeof value !== "string") continue;
|
|
1249
|
-
|
|
1250
|
-
add(token, value.length, "keyword");
|
|
1251
|
-
} else if (token.type === "Identifier" && SOFT_KEYWORDS.has(value)) {
|
|
1252
|
-
add(token, value.length, "keyword");
|
|
1253
|
-
}
|
|
1488
|
+
if (token.type !== "Identifier" || typeof value !== "string" || !SOFT_KEYWORDS.has(value)) continue;
|
|
1489
|
+
add(token, value.length, "keyword", CONTROL_KEYWORDS.has(value) ? ["control"] : []);
|
|
1254
1490
|
}
|
|
1255
1491
|
return { data: encode([...entries.values()]) };
|
|
1256
1492
|
}
|
|
@@ -1456,17 +1692,56 @@ function createServer(connection, options = {}) {
|
|
|
1456
1692
|
const document = documents.get(p.textDocument.uri);
|
|
1457
1693
|
return document ? semanticTokens(analyzer.get(document)) : { data: [] };
|
|
1458
1694
|
});
|
|
1459
|
-
|
|
1460
|
-
void connection.sendDiagnostics({
|
|
1461
|
-
uri: document.uri,
|
|
1462
|
-
version: document.version,
|
|
1463
|
-
diagnostics: diagnostics(analyzer.get(document))
|
|
1464
|
-
});
|
|
1465
|
-
};
|
|
1466
|
-
documents.onDidOpen((e) => publish(e.document));
|
|
1695
|
+
let configUris = /* @__PURE__ */ new Set();
|
|
1467
1696
|
const publishAll = () => {
|
|
1468
|
-
|
|
1697
|
+
const problems = /* @__PURE__ */ new Map();
|
|
1698
|
+
for (const document of documents.all()) {
|
|
1699
|
+
const analysis = analyzer.get(document);
|
|
1700
|
+
void connection.sendDiagnostics({
|
|
1701
|
+
uri: document.uri,
|
|
1702
|
+
version: document.version,
|
|
1703
|
+
diagnostics: [...diagnostics(analysis), ...projectHint(analysis)]
|
|
1704
|
+
});
|
|
1705
|
+
for (const problem of analysis.project.problems) {
|
|
1706
|
+
const uri = uriOfPath(problem.file);
|
|
1707
|
+
const list = problems.get(uri) ?? [];
|
|
1708
|
+
if (!list.some((p) => p.message === problem.message && p.line === problem.line)) list.push(problem);
|
|
1709
|
+
problems.set(uri, list);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
for (const [uri, list] of problems) {
|
|
1713
|
+
void connection.sendDiagnostics({ uri, diagnostics: list.map(problemDiagnostic) });
|
|
1714
|
+
}
|
|
1715
|
+
for (const uri of configUris) {
|
|
1716
|
+
if (!problems.has(uri)) void connection.sendDiagnostics({ uri, diagnostics: [] });
|
|
1717
|
+
}
|
|
1718
|
+
configUris = new Set(problems.keys());
|
|
1719
|
+
};
|
|
1720
|
+
const problemDiagnostic = (problem) => {
|
|
1721
|
+
const line = Math.max((problem.line ?? 1) - 1, 0);
|
|
1722
|
+
const character = Math.max((problem.column ?? 1) - 1, 0);
|
|
1723
|
+
const text = analyzer.readFile(problem.file)?.split("\n")[line] ?? "";
|
|
1724
|
+
const end = Math.max(text.replace(/\r$/, "").trimEnd().length, character + 1);
|
|
1725
|
+
return {
|
|
1726
|
+
range: { start: { line, character }, end: { line, character: end } },
|
|
1727
|
+
severity: import_node.DiagnosticSeverity.Error,
|
|
1728
|
+
source: "luaut",
|
|
1729
|
+
code: "config",
|
|
1730
|
+
message: problem.message
|
|
1731
|
+
};
|
|
1732
|
+
};
|
|
1733
|
+
const projectHint = (analysis) => {
|
|
1734
|
+
const { project } = analysis;
|
|
1735
|
+
if (project.fixed || project.config || !pathOfUri(analysis.uri)) return [];
|
|
1736
|
+
return [{
|
|
1737
|
+
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
|
|
1738
|
+
severity: import_node.DiagnosticSeverity.Information,
|
|
1739
|
+
source: "luaut",
|
|
1740
|
+
code: "no-config",
|
|
1741
|
+
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 }'
|
|
1742
|
+
}];
|
|
1469
1743
|
};
|
|
1744
|
+
documents.onDidOpen(publishAll);
|
|
1470
1745
|
documents.onDidChangeContent(publishAll);
|
|
1471
1746
|
connection.onDidChangeWatchedFiles(publishAll);
|
|
1472
1747
|
documents.onDidClose((e) => {
|